diff --git a/.cloud-init.metadata b/.cloud-init.metadata index 6803c51..0356978 100644 --- a/.cloud-init.metadata +++ b/.cloud-init.metadata @@ -1 +1 @@ -2ae378aa2ae23b34b0ff123623ba5e2fbdc4928d SOURCES/cloud-init-21.1.tar.gz +830185bb5ce87ad86e4d1c0c62329bb255ec1648 SOURCES/cloud-init-22.1.tar.gz diff --git a/.gitignore b/.gitignore index 103bcf7..bf19bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/cloud-init-21.1.tar.gz +SOURCES/cloud-init-22.1.tar.gz diff --git a/SOURCES/0001-Add-initial-redhat-setup.patch b/SOURCES/0001-Add-initial-redhat-setup.patch index b67fcae..d93c32c 100644 --- a/SOURCES/0001-Add-initial-redhat-setup.patch +++ b/SOURCES/0001-Add-initial-redhat-setup.patch @@ -1,8 +1,34 @@ -From 074cb9b011623849cfa95c1d7cc813bb28f03ff0 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo <otubo@redhat.com> -Date: Fri, 7 May 2021 13:36:03 +0200 +From 5e1e568d7085fd4443b4e3ccc492f5e31747e270 Mon Sep 17 00:00:00 2001 +From: Amy Chen <xiachen@redhat.com> +Date: Wed, 20 Apr 2022 10:59:48 +0800 Subject: Add initial redhat setup +Merged patches (22.1) +- d18029bf Add netifaces package as a Requires in cloud-init.spec.template +- 31adf961 Add gdisk and openssl as deps to fix UEFI / Azure initialization +- f4a2905d Add dhcp-client as a dependency +- 290e14cc cloud-init.spec.template: update %systemd_postun parameter +- 9be4ae9b (tag: cloud-init-21.1-1.el8) Update to cloud-init-21.1-1.el8 + +Conflicts: +cloudinit/config/cc_chef.py Using double quotes instead of single quotes + +cloudinit/settings.py +- Using rhel settings +- Using double quotes instead of single quotes + +setup.py +- Following the changes of 21.1 rebase +- Using double quotes instead of single quotes + +redhat/cloud-init.spec.template +- Add the drop-in to the right cloud-init.spec used by our package builder, which is downstream-only part of the bz 2002492 fix. + +redhat/Makefile.common +- Backport the build handling fixes from patch "Update to cloud-init-21.1-1.el8" + +Signed-off-by: Amy Chen <xiachen@redhat.com> + Merged patches (21.1): - 915d30ad Change gating file to correct rhel version - 311f318d Removing net-tools dependency @@ -43,36 +69,36 @@ setup.py: Signed-off-by: Eduardo Otubo <otubo@redhat.com> --- .gitignore | 1 + - cloudinit/config/cc_chef.py | 67 +++- + cloudinit/config/cc_chef.py | 65 ++- cloudinit/settings.py | 7 +- redhat/.gitignore | 1 + - redhat/Makefile | 71 ++++ + redhat/Makefile | 71 +++ redhat/Makefile.common | 37 ++ redhat/cloud-init-tmpfiles.conf | 1 + - redhat/cloud-init.spec.template | 530 ++++++++++++++++++++++++++ + redhat/cloud-init.spec.template | 696 ++++++++++++++++++++++++++ redhat/gating.yaml | 8 + redhat/rpmbuild/BUILD/.gitignore | 3 + redhat/rpmbuild/RPMS/.gitignore | 3 + redhat/rpmbuild/SOURCES/.gitignore | 3 + redhat/rpmbuild/SPECS/.gitignore | 3 + redhat/rpmbuild/SRPMS/.gitignore | 3 + - redhat/scripts/frh.py | 27 ++ - redhat/scripts/git-backport-diff | 327 ++++++++++++++++ - redhat/scripts/git-compile-check | 215 +++++++++++ - redhat/scripts/process-patches.sh | 77 ++++ + redhat/scripts/frh.py | 25 + + redhat/scripts/git-backport-diff | 327 ++++++++++++ + redhat/scripts/git-compile-check | 215 ++++++++ + redhat/scripts/process-patches.sh | 92 ++++ redhat/scripts/tarball_checksum.sh | 3 + rhel/README.rhel | 5 + rhel/cloud-init-tmpfiles.conf | 1 + - rhel/cloud.cfg | 69 ++++ + 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-final.service | 24 + rhel/systemd/cloud-init-local.service | 31 ++ - rhel/systemd/cloud-init.service | 25 ++ + rhel/systemd/cloud-init.service | 25 + rhel/systemd/cloud-init.target | 7 + - setup.py | 23 +- + setup.py | 28 +- tools/read-version | 28 +- - 30 files changed, 1579 insertions(+), 50 deletions(-) + 30 files changed, 1756 insertions(+), 55 deletions(-) create mode 100644 redhat/.gitignore create mode 100644 redhat/Makefile create mode 100644 redhat/Makefile.common @@ -100,7 +126,7 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com> create mode 100644 rhel/systemd/cloud-init.target diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py -index aaf71366..97ef649a 100644 +index fdb3a6e3..d028c548 100644 --- a/cloudinit/config/cc_chef.py +++ b/cloudinit/config/cc_chef.py @@ -6,7 +6,70 @@ @@ -175,38 +201,29 @@ index aaf71366..97ef649a 100644 import itertools import json -@@ -31,7 +94,7 @@ CHEF_DIRS = tuple([ - '/var/lib/chef', - '/var/cache/chef', - '/var/backups/chef', -- '/var/run/chef', -+ '/run/chef', - ]) - REQUIRED_CHEF_DIRS = tuple([ - '/etc/chef', diff --git a/cloudinit/settings.py b/cloudinit/settings.py -index 91e1bfe7..e690c0fd 100644 +index ecc1403b..39650a5b 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py -@@ -47,13 +47,16 @@ CFG_BUILTIN = { +@@ -50,13 +50,16 @@ CFG_BUILTIN = { ], - 'def_log_file': '/var/log/cloud-init.log', - 'log_cfgs': [], -- 'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel', 'root:root'], -+ '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/', + "def_log_file": "/var/log/cloud-init.log", + "log_cfgs": [], +- "syslog_fix_perms": ["syslog:adm", "root:adm", "root:wheel", "root:root"], ++ "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', -+ 'distro': 'rhel', - 'network': {'renderers': None}, +- "distro": "ubuntu", ++ "distro": "rhel", + "network": {"renderers": None}, }, - 'vendor_data': {'enabled': True, 'prefix': []}, + "vendor_data": {"enabled": True, "prefix": []}, diff --git a/rhel/README.rhel b/rhel/README.rhel new file mode 100644 index 00000000..aa29630d @@ -453,70 +470,78 @@ index 00000000..083c3b6f +Description=Cloud-init target +After=multi-user.target diff --git a/setup.py b/setup.py -index cbacf48e..d5cd01a4 100755 +index a9132d2c..3c377eaa 100755 --- a/setup.py +++ b/setup.py -@@ -125,14 +125,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/*.target')) -- if (is_f(f) and not is_generator(f))], -- 'systemd.generators': [ +@@ -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)], +- 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 = { -@@ -142,9 +134,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/', +@@ -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()]) -@@ -245,14 +234,11 @@ if not in_virtualenv(): - INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k] - - data_files = [ -- (ETC + '/cloud', [render_tmpl("config/cloud.cfg.tmpl")]), -+ (ETC + '/bash_completion.d', ['bash_completion/cloud-init']), - (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')), - (ETC + '/cloud/templates', glob('templates/*')), -- (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify', -- 'tools/uncloud-init', -+ (USR_LIB_EXEC + '/cloud-init', ['tools/uncloud-init', - 'tools/write-ssh-key-fingerprints']), -- (USR + '/share/bash-completion/completions', -- ['bash_completion/cloud-init']), - (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]), - (USR + '/share/doc/cloud-init/examples', - [f for f in glob('doc/examples/*') if is_f(f)]), -@@ -263,8 +249,7 @@ if not platform.system().endswith('BSD'): - data_files.extend([ - (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')]) - ]) - # Use a subclass for install that handles - # adding on the right init system configuration files -@@ -286,8 +271,6 @@ setuptools.setup( - scripts=['tools/cloud-init-per'], - license='Dual-licensed under GPLv3 or Apache 2.0', +@@ -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', + "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 @@ -557,5 +582,5 @@ index 02c90643..79755f78 100755 # version is X.Y.Z[+xxx.gHASH] # version_long is None or X.Y.Z-xxx-gHASH -- -2.27.0 +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 index 3dc704f..60c0a2a 100644 --- 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 @@ -1,4 +1,4 @@ -From 472c2b5d4342b6ab6ce1584dc39bed0e6c1ca2e7 Mon Sep 17 00:00:00 2001 +From e0dc628ac553072891fa6607dc91b652efd99be2 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo <otubo@redhat.com> Date: Fri, 7 May 2021 13:36:06 +0200 Subject: Do not write NM_CONTROLLED=no in generated interface config files @@ -12,28 +12,27 @@ X-downstream-only: true Signed-off-by: Eduardo Otubo <otubo@redhat.com> Signed-off-by: Ryan McCabe <rmccabe@redhat.com> --- - cloudinit/net/sysconfig.py | 2 +- + cloudinit/net/sysconfig.py | 1 - tests/unittests/test_net.py | 28 ---------------------------- - 2 files changed, 1 insertion(+), 29 deletions(-) + 2 files changed, 29 deletions(-) diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 99a4bae4..3d276666 100644 +index ba85c4f6..e06ddee7 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py -@@ -289,7 +289,7 @@ class Renderer(renderer.Renderer): - # details about this) - - iface_defaults = { -- 'rhel': {'ONBOOT': True, 'USERCTL': False, 'NM_CONTROLLED': False, -+ 'rhel': {'ONBOOT': True, 'USERCTL': False, - 'BOOTPROTO': 'none'}, - 'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'}, - } +@@ -336,7 +336,6 @@ class Renderer(renderer.Renderer): + "rhel": { + "ONBOOT": True, + "USERCTL": False, +- "NM_CONTROLLED": False, + "BOOTPROTO": "none", + }, + "suse": {"BOOTPROTO": "static", "STARTMODE": "auto"}, diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index 38d934d4..c67b5fcc 100644 +index 47e4ba00..591241b3 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py -@@ -535,7 +535,6 @@ GATEWAY=172.19.3.254 +@@ -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 @@ -41,7 +40,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -633,7 +632,6 @@ IPADDR=172.19.1.34 +@@ -712,7 +711,6 @@ IPADDR=172.19.1.34 IPADDR1=10.0.0.10 NETMASK=255.255.252.0 NETMASK1=255.255.255.0 @@ -49,7 +48,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -756,7 +754,6 @@ IPV6_AUTOCONF=no +@@ -874,7 +872,6 @@ IPV6_AUTOCONF=no IPV6_DEFAULTGW=2001:DB8::1 IPV6_FORCE_ACCEPT_RA=no NETMASK=255.255.252.0 @@ -57,23 +56,23 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -884,7 +881,6 @@ NETWORK_CONFIGS = { +@@ -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"""), -@@ -901,7 +897,6 @@ NETWORK_CONFIGS = { + 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"""), -@@ -1032,7 +1027,6 @@ NETWORK_CONFIGS = { + USERCTL=no""" +@@ -1244,7 +1239,6 @@ NETWORK_CONFIGS = { IPV6_AUTOCONF=no IPV6_FORCE_ACCEPT_RA=no NETMASK=255.255.255.0 @@ -81,15 +80,15 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -1737,7 +1731,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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"""), -@@ -1745,7 +1738,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + 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 @@ -97,7 +96,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes PHYSDEV=bond0 USERCTL=no -@@ -1763,7 +1755,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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 @@ -105,15 +104,15 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes PRIO=22 STP=no -@@ -1773,7 +1764,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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"""), -@@ -1790,7 +1780,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + 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 @@ -121,7 +120,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes PHYSDEV=eth0 USERCTL=no -@@ -1800,7 +1789,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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 @@ -129,7 +128,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -1810,7 +1798,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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 @@ -137,31 +136,31 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -1820,7 +1807,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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"""), -@@ -1829,7 +1815,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + 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"""), -@@ -1838,7 +1823,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + 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"""), -@@ -2294,7 +2278,6 @@ iface bond0 inet6 static + USERCTL=no""" +@@ -2689,7 +2673,6 @@ iface bond0 inet6 static MTU=9000 NETMASK=255.255.255.0 NETMASK1=255.255.255.0 @@ -169,7 +168,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Bond USERCTL=no -@@ -2304,7 +2287,6 @@ iface bond0 inet6 static +@@ -2701,7 +2684,6 @@ iface bond0 inet6 static DEVICE=bond0s0 HWADDR=aa:bb:cc:dd:e8:00 MASTER=bond0 @@ -177,7 +176,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -2326,7 +2308,6 @@ iface bond0 inet6 static +@@ -2729,7 +2711,6 @@ iface bond0 inet6 static DEVICE=bond0s1 HWADDR=aa:bb:cc:dd:e8:01 MASTER=bond0 @@ -185,15 +184,15 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -2383,7 +2364,6 @@ iface bond0 inet6 static +@@ -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"""), -@@ -2402,7 +2382,6 @@ iface bond0 inet6 static + USERCTL=no""" +@@ -2815,7 +2795,6 @@ iface bond0 inet6 static MTU=2222 NETMASK=255.255.255.0 NETMASK1=255.255.255.0 @@ -201,7 +200,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes PHYSDEV=en0 USERCTL=no -@@ -2467,7 +2446,6 @@ iface bond0 inet6 static +@@ -2890,7 +2869,6 @@ iface bond0 inet6 static DEVICE=br0 IPADDR=192.168.2.2 NETMASK=255.255.255.0 @@ -209,7 +208,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes PRIO=22 STP=no -@@ -2591,7 +2569,6 @@ iface bond0 inet6 static +@@ -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 @@ -217,7 +216,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=no TYPE=Ethernet USERCTL=no -@@ -2601,7 +2578,6 @@ iface bond0 inet6 static +@@ -3044,7 +3021,6 @@ iface bond0 inet6 static DEVICE=eth1 HWADDR=52:54:00:12:34:aa MTU=1480 @@ -225,7 +224,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -2610,7 +2586,6 @@ iface bond0 inet6 static +@@ -3055,7 +3031,6 @@ iface bond0 inet6 static BOOTPROTO=none DEVICE=eth2 HWADDR=52:54:00:12:34:ff @@ -233,7 +232,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=no TYPE=Ethernet USERCTL=no -@@ -3027,7 +3002,6 @@ class TestRhelSysConfigRendering(CiTestCase): +@@ -3628,7 +3603,6 @@ class TestRhelSysConfigRendering(CiTestCase): BOOTPROTO=dhcp DEVICE=eth1000 HWADDR=07-1c-c6-75-a4-be @@ -241,7 +240,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -3148,7 +3122,6 @@ GATEWAY=10.0.2.2 +@@ -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 @@ -249,7 +248,7 @@ index 38d934d4..c67b5fcc 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -3218,7 +3191,6 @@ USERCTL=no +@@ -3910,7 +3883,6 @@ USERCTL=no # BOOTPROTO=dhcp DEVICE=eth0 @@ -258,5 +257,5 @@ index 38d934d4..c67b5fcc 100644 TYPE=Ethernet USERCTL=no -- -2.27.0 +2.31.1 diff --git a/SOURCES/0003-limit-permissions-on-def_log_file.patch b/SOURCES/0003-limit-permissions-on-def_log_file.patch index 941adaf..6f58247 100644 --- a/SOURCES/0003-limit-permissions-on-def_log_file.patch +++ b/SOURCES/0003-limit-permissions-on-def_log_file.patch @@ -1,4 +1,4 @@ -From 6134624f10ef56534e37624adc12f11b09910591 Mon Sep 17 00:00:00 2001 +From cb7b35ca10c82c9725c3527e3ec5fb8cb7c61bc0 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo <otubo@redhat.com> Date: Fri, 7 May 2021 13:36:08 +0200 Subject: limit permissions on def_log_file @@ -22,31 +22,31 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com> 3 files changed, 6 insertions(+) diff --git a/cloudinit/settings.py b/cloudinit/settings.py -index e690c0fd..43a1490c 100644 +index 39650a5b..3c2145e9 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py -@@ -46,6 +46,7 @@ CFG_BUILTIN = { - 'None', +@@ -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, + "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 3ef4491c..83e25dd1 100644 +index 3f17294b..61db1dbd 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py -@@ -147,6 +147,7 @@ class Init(object): +@@ -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') + 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, preserve_mode=True) - perms = self.cfg.get('syslog_fix_perms') + 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 de9a0f87..bb33ad45 100644 +index a2b4a3fa..0ccf3147 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -414,10 +414,14 @@ timezone: US/Eastern @@ -65,5 +65,5 @@ index de9a0f87..bb33ad45 100644 # you can set passwords for a user or multiple users -- -2.27.0 +2.31.1 diff --git a/SOURCES/0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch b/SOURCES/0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch new file mode 100644 index 0000000..5c5a144 --- /dev/null +++ b/SOURCES/0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch @@ -0,0 +1,52 @@ +From ffa647e83efd4293bd027e9e390274aad8a12d94 Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo <otubo@redhat.com> +Date: Fri, 7 May 2021 13:36:13 +0200 +Subject: include 'NOZEROCONF=yes' in /etc/sysconfig/network + +RH-Author: Eduardo Otubo <otubo@redhat.com> +Message-id: <20190320114559.23708-1-otubo@redhat.com> +Patchwork-id: 84937 +O-Subject: [RHEL-7.7 cloud-init PATCH] include 'NOZEROCONF=yes' in /etc/sysconfig/network +Bugzilla: 1653131 +RH-Acked-by: Cathy Avery <cavery@redhat.com> +RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> + +The option NOZEROCONF=yes is not included by default in +/etc/sysconfig/network, which is required by Overcloud instances. The +patch also includes tests for the modifications. + +X-downstream-only: yes +Resolves: rhbz#1653131 + +Signed-off-by: Eduardo Otubo <otubo@redhat.com> +Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> +--- + cloudinit/net/sysconfig.py | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index e06ddee7..362e8d19 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -1038,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") +-- +2.31.1 + diff --git a/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch b/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch deleted file mode 100644 index 4d5a0d2..0000000 --- a/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 699d37a6ff3e343e214943794aac09e4156c2b2b Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo <otubo@redhat.com> -Date: Fri, 7 May 2021 13:36:10 +0200 -Subject: sysconfig: Don't write BOOTPROTO=dhcp for ipv6 dhcp - -Don't write BOOTPROTO=dhcp for ipv6 dhcp, as BOOTPROTO applies -only to ipv4. Explicitly write IPV6_AUTOCONF=no for dhcp on ipv6. - -X-downstream-only: yes - -Resolves: rhbz#1519271 -Signed-off-by: Ryan McCabe <rmccabe@redhat.com> - -Merged patches (19.4): -- 6444df4 sysconfig: Don't disable IPV6_AUTOCONF - -Signed-off-by: Eduardo Otubo <otubo@redhat.com> ---- - tests/unittests/test_net.py | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index c67b5fcc..4ea0e597 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -1729,6 +1729,7 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - BOOTPROTO=none - DEVICE=bond0 - DHCPV6C=yes -+ IPV6_AUTOCONF=no - IPV6INIT=yes - MACADDR=aa:bb:cc:dd:ee:ff - ONBOOT=yes --- -2.27.0 - diff --git a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch b/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch deleted file mode 100644 index 100d3a2..0000000 --- a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch +++ /dev/null @@ -1,57 +0,0 @@ -From ccc75c1be3ae08d813193071c798fc905b5c03e5 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo <otubo@redhat.com> -Date: Fri, 7 May 2021 13:36:12 +0200 -Subject: DataSourceAzure.py: use hostnamectl to set hostname - -RH-Author: Vitaly Kuznetsov <vkuznets@redhat.com> -Message-id: <20180417130754.12918-3-vkuznets@redhat.com> -Patchwork-id: 79659 -O-Subject: [RHEL7.6/7.5.z cloud-init PATCH 2/2] DataSourceAzure.py: use hostnamectl to set hostname -Bugzilla: 1568717 -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> -RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> -RH-Acked-by: Cathy Avery <cavery@redhat.com> - -The right way to set hostname in RHEL7 is: - - $ hostnamectl set-hostname HOSTNAME - -DataSourceAzure, however, uses: - $ hostname HOSTSNAME - -instead and this causes problems. We can't simply change -'BUILTIN_DS_CONFIG' in DataSourceAzure.py as 'hostname' is being used -for both getting and setting the hostname. - -Long term, this should be fixed in a different way. Cloud-init -has distro-specific hostname setting/getting (see -cloudinit/distros/rhel.py) and DataSourceAzure.py needs to be switched -to use these. - -Resolves: rhbz#1434109 - -X-downstream-only: yes - -Signed-off-by: Eduardo Otubo <otubo@redhat.com> -Signed-off-by: Vitaly Kuznetsov <vkuznets@redhat.com> -Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> ---- - cloudinit/sources/DataSourceAzure.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index cee630f7..553b5a7e 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -296,7 +296,7 @@ def get_hostname(hostname_command='hostname'): - - - def set_hostname(hostname, hostname_command='hostname'): -- subp.subp([hostname_command, hostname]) -+ util.subp(['hostnamectl', 'set-hostname', str(hostname)]) - - - @azure_ds_telemetry_reporter --- -2.27.0 - diff --git a/SOURCES/0005-Remove-race-condition-between-cloud-init-and-Network.patch b/SOURCES/0005-Remove-race-condition-between-cloud-init-and-Network.patch new file mode 100644 index 0000000..478e5ab --- /dev/null +++ b/SOURCES/0005-Remove-race-condition-between-cloud-init-and-Network.patch @@ -0,0 +1,148 @@ +From 386f0a82bfdfd62e506bf4251c17263260d3250a Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo <otubo@redhat.com> +Date: Fri, 7 May 2021 13:36:14 +0200 +Subject: Remove race condition between cloud-init and NetworkManager + +Message-id: <20200302104635.11648-1-otubo@redhat.com> +Patchwork-id: 94098 +O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Remove race condition between cloud-init and NetworkManager +Bugzilla: 1807797 +RH-Acked-by: Cathy Avery <cavery@redhat.com> +RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> + +BZ: 1748015 +BRANCH: rhel7/master-18.5 +BREW: 26924611 + +BZ: 1807797 +BRANCH: rhel820/master-18.5 +BREW: 26924957 + +cloud-init service is set to start before NetworkManager service starts, +but this does not avoid a race condition between them. NetworkManager +starts before cloud-init can write `dns=none' to the file: +/etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager +doesn't read the configuration and erases all resolv.conf values upon +shutdown. On the next reboot neither cloud-init or NetworkManager will +write anything to resolv.conf, leaving it blank. + +This patch introduces a NM reload (try-restart) at the end of cloud-init +start up so it won't erase resolv.conf upon first shutdown. + +x-downstream-only: yes +resolves: rhbz#1748015, rhbz#1807797 and rhbz#1804780 + +Signed-off-by: Eduardo Otubo <otubo@redhat.com> +Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> + +This commit is a squash and also includes the folloowing commits: + +commit 316a17b7c02a87fa9b2981535be0b20d165adc46 +Author: Eduardo Otubo <otubo@redhat.com> +Date: Mon Jun 1 11:58:06 2020 +0200 + + Make cloud-init.service execute after network is up + + RH-Author: Eduardo Otubo <otubo@redhat.com> + Message-id: <20200526090804.2047-1-otubo@redhat.com> + Patchwork-id: 96809 + O-Subject: [RHEL-8.2.1 cloud-init PATCH] Make cloud-init.service execute after network is up + Bugzilla: 1803928 + RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> + RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> + + cloud-init.service needs to wait until network is fully up before + continuing executing and configuring its service. + + Signed-off-by: Eduardo Otubo <otubo@redhat.com> + + x-downstream-only: yes + Resolves: rhbz#1831646 + Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> + +commit 0422ba0e773d1a8257a3f2bf3db05f3bc7917eb7 +Author: Eduardo Otubo <otubo@redhat.com> +Date: Thu May 28 08:44:08 2020 +0200 + + Remove race condition between cloud-init and NetworkManager + + RH-Author: Eduardo Otubo <otubo@redhat.com> + Message-id: <20200327121911.17699-1-otubo@redhat.com> + Patchwork-id: 94453 + O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCHv2] Remove race condition between cloud-init and NetworkManager + Bugzilla: 1840648 + RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> + RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> + RH-Acked-by: Cathy Avery <cavery@redhat.com> + + cloud-init service is set to start before NetworkManager service starts, + but this does not avoid a race condition between them. NetworkManager + starts before cloud-init can write `dns=none' to the file: + /etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager + doesn't read the configuration and erases all resolv.conf values upon + shutdown. On the next reboot neither cloud-init or NetworkManager will + write anything to resolv.conf, leaving it blank. + + This patch introduces a NM reload (try-reload-or-restart) at the end of cloud-init + start up so it won't erase resolv.conf upon first shutdown. + + x-downstream-only: yes + + Signed-off-by: Eduardo Otubo otubo@redhat.com + Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> + +commit e0b48a936433faea7f56dbc29dda35acf7d375f7 +Author: Eduardo Otubo <otubo@redhat.com> +Date: Thu May 28 08:44:06 2020 +0200 + + Enable ssh_deletekeys by default + + RH-Author: Eduardo Otubo <otubo@redhat.com> + Message-id: <20200317091705.15715-1-otubo@redhat.com> + Patchwork-id: 94365 + O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Enable ssh_deletekeys by default + Bugzilla: 1814152 + RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> + RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> + + The configuration option ssh_deletekeys will trigger the generation + of new ssh keys for every new instance deployed. + + x-downstream-only: yes + resolves: rhbz#1814152 + + Signed-off-by: Eduardo Otubo <otubo@redhat.com> + Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> +--- + rhel/cloud.cfg | 2 +- + rhel/systemd/cloud-init.service | 1 + + 2 files changed, 2 insertions(+), 1 deletion(-) + +diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg +index 82e8bf62..9ecba215 100644 +--- a/rhel/cloud.cfg ++++ b/rhel/cloud.cfg +@@ -6,7 +6,7 @@ ssh_pwauth: 0 + + mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2'] + resize_rootfs_tmp: /dev +-ssh_deletekeys: 0 ++ssh_deletekeys: 1 + ssh_genkeytypes: ~ + syslog_fix_perms: ~ + disable_vmware_customization: false +diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service +index d0023a05..0b3d796d 100644 +--- a/rhel/systemd/cloud-init.service ++++ b/rhel/systemd/cloud-init.service +@@ -5,6 +5,7 @@ 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 +-- +2.31.1 + diff --git a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch b/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch deleted file mode 100644 index 6276255..0000000 --- a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch +++ /dev/null @@ -1,65 +0,0 @@ -From dfea0490b899804761fbd7aa23822783d7c36ec5 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo <otubo@redhat.com> -Date: Fri, 7 May 2021 13:36:13 +0200 -Subject: include 'NOZEROCONF=yes' in /etc/sysconfig/network - -RH-Author: Eduardo Otubo <otubo@redhat.com> -Message-id: <20190320114559.23708-1-otubo@redhat.com> -Patchwork-id: 84937 -O-Subject: [RHEL-7.7 cloud-init PATCH] include 'NOZEROCONF=yes' in /etc/sysconfig/network -Bugzilla: 1653131 -RH-Acked-by: Cathy Avery <cavery@redhat.com> -RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> -RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> - -The option NOZEROCONF=yes is not included by default in -/etc/sysconfig/network, which is required by Overcloud instances. The -patch also includes tests for the modifications. - -X-downstream-only: yes -Resolves: rhbz#1653131 - -Signed-off-by: Eduardo Otubo <otubo@redhat.com> -Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> ---- - cloudinit/net/sysconfig.py | 11 ++++++++++- - tests/unittests/test_net.py | 1 - - 2 files changed, 10 insertions(+), 2 deletions(-) - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 3d276666..d5440998 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -925,7 +925,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 4ea0e597..c67b5fcc 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -1729,7 +1729,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - BOOTPROTO=none - DEVICE=bond0 - DHCPV6C=yes -- IPV6_AUTOCONF=no - IPV6INIT=yes - MACADDR=aa:bb:cc:dd:ee:ff - ONBOOT=yes --- -2.27.0 - 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..e596836 --- /dev/null +++ b/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch @@ -0,0 +1,65 @@ +From b545a0cbabe8924d048b7172b30e7aad59ed32d5 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +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 <eesposit@redhat.com> +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 <otubo@redhat.com> +RH-Acked-by: Cathy Avery <cavery@redhat.com> +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> + +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 <eesposit@redhat.com> +--- + 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 3c2145e9..71672e10 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 9ecba215..cbee197a 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', '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/0007-Remove-race-condition-between-cloud-init-and-Network.patch b/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch deleted file mode 100644 index 9c9e4cc..0000000 --- a/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch +++ /dev/null @@ -1,148 +0,0 @@ -From 24894dcf45a307f44e29dc5d5b2d864b75fd982c Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo <otubo@redhat.com> -Date: Fri, 7 May 2021 13:36:14 +0200 -Subject: Remove race condition between cloud-init and NetworkManager - -Message-id: <20200302104635.11648-1-otubo@redhat.com> -Patchwork-id: 94098 -O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Remove race condition between cloud-init and NetworkManager -Bugzilla: 1807797 -RH-Acked-by: Cathy Avery <cavery@redhat.com> -RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> - -BZ: 1748015 -BRANCH: rhel7/master-18.5 -BREW: 26924611 - -BZ: 1807797 -BRANCH: rhel820/master-18.5 -BREW: 26924957 - -cloud-init service is set to start before NetworkManager service starts, -but this does not avoid a race condition between them. NetworkManager -starts before cloud-init can write `dns=none' to the file: -/etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager -doesn't read the configuration and erases all resolv.conf values upon -shutdown. On the next reboot neither cloud-init or NetworkManager will -write anything to resolv.conf, leaving it blank. - -This patch introduces a NM reload (try-restart) at the end of cloud-init -start up so it won't erase resolv.conf upon first shutdown. - -x-downstream-only: yes -resolves: rhbz#1748015, rhbz#1807797 and rhbz#1804780 - -Signed-off-by: Eduardo Otubo <otubo@redhat.com> -Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> - -This commit is a squash and also includes the folloowing commits: - -commit 316a17b7c02a87fa9b2981535be0b20d165adc46 -Author: Eduardo Otubo <otubo@redhat.com> -Date: Mon Jun 1 11:58:06 2020 +0200 - - Make cloud-init.service execute after network is up - - RH-Author: Eduardo Otubo <otubo@redhat.com> - Message-id: <20200526090804.2047-1-otubo@redhat.com> - Patchwork-id: 96809 - O-Subject: [RHEL-8.2.1 cloud-init PATCH] Make cloud-init.service execute after network is up - Bugzilla: 1803928 - RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> - RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> - - cloud-init.service needs to wait until network is fully up before - continuing executing and configuring its service. - - Signed-off-by: Eduardo Otubo <otubo@redhat.com> - - x-downstream-only: yes - Resolves: rhbz#1831646 - Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> - -commit 0422ba0e773d1a8257a3f2bf3db05f3bc7917eb7 -Author: Eduardo Otubo <otubo@redhat.com> -Date: Thu May 28 08:44:08 2020 +0200 - - Remove race condition between cloud-init and NetworkManager - - RH-Author: Eduardo Otubo <otubo@redhat.com> - Message-id: <20200327121911.17699-1-otubo@redhat.com> - Patchwork-id: 94453 - O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCHv2] Remove race condition between cloud-init and NetworkManager - Bugzilla: 1840648 - RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> - RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> - RH-Acked-by: Cathy Avery <cavery@redhat.com> - - cloud-init service is set to start before NetworkManager service starts, - but this does not avoid a race condition between them. NetworkManager - starts before cloud-init can write `dns=none' to the file: - /etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager - doesn't read the configuration and erases all resolv.conf values upon - shutdown. On the next reboot neither cloud-init or NetworkManager will - write anything to resolv.conf, leaving it blank. - - This patch introduces a NM reload (try-reload-or-restart) at the end of cloud-init - start up so it won't erase resolv.conf upon first shutdown. - - x-downstream-only: yes - - Signed-off-by: Eduardo Otubo otubo@redhat.com - Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> - -commit e0b48a936433faea7f56dbc29dda35acf7d375f7 -Author: Eduardo Otubo <otubo@redhat.com> -Date: Thu May 28 08:44:06 2020 +0200 - - Enable ssh_deletekeys by default - - RH-Author: Eduardo Otubo <otubo@redhat.com> - Message-id: <20200317091705.15715-1-otubo@redhat.com> - Patchwork-id: 94365 - O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Enable ssh_deletekeys by default - Bugzilla: 1814152 - RH-Acked-by: Mohammed Gamal <mgamal@redhat.com> - RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> - - The configuration option ssh_deletekeys will trigger the generation - of new ssh keys for every new instance deployed. - - x-downstream-only: yes - resolves: rhbz#1814152 - - Signed-off-by: Eduardo Otubo <otubo@redhat.com> - Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com> ---- - rhel/cloud.cfg | 2 +- - rhel/systemd/cloud-init.service | 1 + - 2 files changed, 2 insertions(+), 1 deletion(-) - -diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg -index 82e8bf62..9ecba215 100644 ---- a/rhel/cloud.cfg -+++ b/rhel/cloud.cfg -@@ -6,7 +6,7 @@ ssh_pwauth: 0 - - mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2'] - resize_rootfs_tmp: /dev --ssh_deletekeys: 0 -+ssh_deletekeys: 1 - ssh_genkeytypes: ~ - syslog_fix_perms: ~ - disable_vmware_customization: false -diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service -index d0023a05..0b3d796d 100644 ---- a/rhel/systemd/cloud-init.service -+++ b/rhel/systemd/cloud-init.service -@@ -5,6 +5,7 @@ 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 --- -2.27.0 - diff --git a/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch b/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch deleted file mode 100644 index 38f08cc..0000000 --- a/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch +++ /dev/null @@ -1,496 +0,0 @@ -From b48dda73da94782d7ab0c455fa382d3a5ef3c419 Mon Sep 17 00:00:00 2001 -From: Daniel Watkins <oddbloke@ubuntu.com> -Date: Mon, 8 Mar 2021 12:50:57 -0500 -Subject: net: exclude OVS internal interfaces in get_interfaces (#829) - -`get_interfaces` is used to in two ways, broadly: firstly, to determine -the available interfaces when converting cloud network configuration -formats to cloud-init's network configuration formats; and, secondly, to -ensure that any interfaces which are specified in network configuration -are (a) available, and (b) named correctly. The first of these is -unaffected by this commit, as no clouds support Open vSwitch -configuration in their network configuration formats. - -For the second, we check that MAC addresses of physical devices are -unique. In some OVS configurations, there are OVS-created devices which -have duplicate MAC addresses, either with each other or with physical -devices. As these interfaces are created by OVS, we can be confident -that (a) they will be available when appropriate, and (b) that OVS will -name them correctly. As such, this commit excludes any OVS-internal -interfaces from the set of interfaces returned by `get_interfaces`. - -LP: #1912844 ---- - cloudinit/net/__init__.py | 62 +++++++++ - cloudinit/net/tests/test_init.py | 119 ++++++++++++++++++ - .../sources/helpers/tests/test_openstack.py | 5 + - cloudinit/sources/tests/test_oracle.py | 4 + - .../integration_tests/bugs/test_lp1912844.py | 103 +++++++++++++++ - .../test_datasource/test_configdrive.py | 8 ++ - tests/unittests/test_net.py | 20 +++ - 7 files changed, 321 insertions(+) - create mode 100644 tests/integration_tests/bugs/test_lp1912844.py - -diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py -index de65e7af..385b7bcc 100644 ---- a/cloudinit/net/__init__.py -+++ b/cloudinit/net/__init__.py -@@ -6,6 +6,7 @@ - # This file is part of cloud-init. See LICENSE file for license information. - - import errno -+import functools - import ipaddress - import logging - import os -@@ -19,6 +20,19 @@ from cloudinit.url_helper import UrlError, readurl - LOG = logging.getLogger(__name__) - SYS_CLASS_NET = "/sys/class/net/" - DEFAULT_PRIMARY_INTERFACE = 'eth0' -+OVS_INTERNAL_INTERFACE_LOOKUP_CMD = [ -+ "ovs-vsctl", -+ "--format", -+ "csv", -+ "--no-headings", -+ "--timeout", -+ "10", -+ "--columns", -+ "name", -+ "find", -+ "interface", -+ "type=internal", -+] - - - def natural_sort_key(s, _nsre=re.compile('([0-9]+)')): -@@ -133,6 +147,52 @@ def master_is_openvswitch(devname): - return os.path.exists(ovs_path) - - -+@functools.lru_cache(maxsize=None) -+def openvswitch_is_installed() -> bool: -+ """Return a bool indicating if Open vSwitch is installed in the system.""" -+ ret = bool(subp.which("ovs-vsctl")) -+ if not ret: -+ LOG.debug( -+ "ovs-vsctl not in PATH; not detecting Open vSwitch interfaces" -+ ) -+ return ret -+ -+ -+@functools.lru_cache(maxsize=None) -+def get_ovs_internal_interfaces() -> list: -+ """Return a list of the names of OVS internal interfaces on the system. -+ -+ These will all be strings, and are used to exclude OVS-specific interface -+ from cloud-init's network configuration handling. -+ """ -+ try: -+ out, _err = subp.subp(OVS_INTERNAL_INTERFACE_LOOKUP_CMD) -+ except subp.ProcessExecutionError as exc: -+ if "database connection failed" in exc.stderr: -+ LOG.info( -+ "Open vSwitch is not yet up; no interfaces will be detected as" -+ " OVS-internal" -+ ) -+ return [] -+ raise -+ else: -+ return out.splitlines() -+ -+ -+def is_openvswitch_internal_interface(devname: str) -> bool: -+ """Returns True if this is an OVS internal interface. -+ -+ If OVS is not installed or not yet running, this will return False. -+ """ -+ if not openvswitch_is_installed(): -+ return False -+ ovs_bridges = get_ovs_internal_interfaces() -+ if devname in ovs_bridges: -+ LOG.debug("Detected %s as an OVS interface", devname) -+ return True -+ return False -+ -+ - def is_netfailover(devname, driver=None): - """ netfailover driver uses 3 nics, master, primary and standby. - this returns True if the device is either the primary or standby -@@ -884,6 +944,8 @@ def get_interfaces(blacklist_drivers=None) -> list: - # skip nics that have no mac (00:00....) - if name != 'lo' and mac == zero_mac[:len(mac)]: - continue -+ if is_openvswitch_internal_interface(name): -+ continue - # skip nics that have drivers blacklisted - driver = device_driver(name) - if driver in blacklist_drivers: -diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py -index 0535387a..946f8ee2 100644 ---- a/cloudinit/net/tests/test_init.py -+++ b/cloudinit/net/tests/test_init.py -@@ -391,6 +391,10 @@ class TestGetDeviceList(CiTestCase): - self.assertCountEqual(['eth0', 'eth1'], net.get_devicelist()) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False), -+) - class TestGetInterfaceMAC(CiTestCase): - - def setUp(self): -@@ -1224,6 +1228,121 @@ class TestNetFailOver(CiTestCase): - self.assertFalse(net.is_netfailover(devname, driver)) - - -+class TestOpenvswitchIsInstalled: -+ """Test cloudinit.net.openvswitch_is_installed. -+ -+ Uses the ``clear_lru_cache`` local autouse fixture to allow us to test -+ despite the ``lru_cache`` decorator on the unit under test. -+ """ -+ -+ @pytest.fixture(autouse=True) -+ def clear_lru_cache(self): -+ net.openvswitch_is_installed.cache_clear() -+ -+ @pytest.mark.parametrize( -+ "expected,which_return", [(True, "/some/path"), (False, None)] -+ ) -+ @mock.patch("cloudinit.net.subp.which") -+ def test_mirrors_which_result(self, m_which, expected, which_return): -+ m_which.return_value = which_return -+ assert expected == net.openvswitch_is_installed() -+ -+ @mock.patch("cloudinit.net.subp.which") -+ def test_only_calls_which_once(self, m_which): -+ net.openvswitch_is_installed() -+ net.openvswitch_is_installed() -+ assert 1 == m_which.call_count -+ -+ -+@mock.patch("cloudinit.net.subp.subp", return_value=("", "")) -+class TestGetOVSInternalInterfaces: -+ """Test cloudinit.net.get_ovs_internal_interfaces. -+ -+ Uses the ``clear_lru_cache`` local autouse fixture to allow us to test -+ despite the ``lru_cache`` decorator on the unit under test. -+ """ -+ @pytest.fixture(autouse=True) -+ def clear_lru_cache(self): -+ net.get_ovs_internal_interfaces.cache_clear() -+ -+ def test_command_used(self, m_subp): -+ """Test we use the correct command when we call subp""" -+ net.get_ovs_internal_interfaces() -+ -+ assert [ -+ mock.call(net.OVS_INTERNAL_INTERFACE_LOOKUP_CMD) -+ ] == m_subp.call_args_list -+ -+ def test_subp_contents_split_and_returned(self, m_subp): -+ """Test that the command output is appropriately mangled.""" -+ stdout = "iface1\niface2\niface3\n" -+ m_subp.return_value = (stdout, "") -+ -+ assert [ -+ "iface1", -+ "iface2", -+ "iface3", -+ ] == net.get_ovs_internal_interfaces() -+ -+ def test_database_connection_error_handled_gracefully(self, m_subp): -+ """Test that the error indicating OVS is down is handled gracefully.""" -+ m_subp.side_effect = ProcessExecutionError( -+ stderr="database connection failed" -+ ) -+ -+ assert [] == net.get_ovs_internal_interfaces() -+ -+ def test_other_errors_raised(self, m_subp): -+ """Test that only database connection errors are handled.""" -+ m_subp.side_effect = ProcessExecutionError() -+ -+ with pytest.raises(ProcessExecutionError): -+ net.get_ovs_internal_interfaces() -+ -+ def test_only_runs_once(self, m_subp): -+ """Test that we cache the value.""" -+ net.get_ovs_internal_interfaces() -+ net.get_ovs_internal_interfaces() -+ -+ assert 1 == m_subp.call_count -+ -+ -+@mock.patch("cloudinit.net.get_ovs_internal_interfaces") -+@mock.patch("cloudinit.net.openvswitch_is_installed") -+class TestIsOpenVSwitchInternalInterface: -+ def test_false_if_ovs_not_installed( -+ self, m_openvswitch_is_installed, _m_get_ovs_internal_interfaces -+ ): -+ """Test that OVS' absence returns False.""" -+ m_openvswitch_is_installed.return_value = False -+ -+ assert not net.is_openvswitch_internal_interface("devname") -+ -+ @pytest.mark.parametrize( -+ "detected_interfaces,devname,expected_return", -+ [ -+ ([], "devname", False), -+ (["notdevname"], "devname", False), -+ (["devname"], "devname", True), -+ (["some", "other", "devices", "and", "ours"], "ours", True), -+ ], -+ ) -+ def test_return_value_based_on_detected_interfaces( -+ self, -+ m_openvswitch_is_installed, -+ m_get_ovs_internal_interfaces, -+ detected_interfaces, -+ devname, -+ expected_return, -+ ): -+ """Test that the detected interfaces are used correctly.""" -+ m_openvswitch_is_installed.return_value = True -+ m_get_ovs_internal_interfaces.return_value = detected_interfaces -+ assert expected_return == net.is_openvswitch_internal_interface( -+ devname -+ ) -+ -+ - class TestIsIpAddress: - """Tests for net.is_ip_address. - -diff --git a/cloudinit/sources/helpers/tests/test_openstack.py b/cloudinit/sources/helpers/tests/test_openstack.py -index 2bde1e3f..95fb9743 100644 ---- a/cloudinit/sources/helpers/tests/test_openstack.py -+++ b/cloudinit/sources/helpers/tests/test_openstack.py -@@ -1,10 +1,15 @@ - # This file is part of cloud-init. See LICENSE file for license information. - # ./cloudinit/sources/helpers/tests/test_openstack.py -+from unittest import mock - - from cloudinit.sources.helpers import openstack - from cloudinit.tests import helpers as test_helpers - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestConvertNetJson(test_helpers.CiTestCase): - - def test_phy_types(self): -diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py -index a7bbdfd9..dcf33b9b 100644 ---- a/cloudinit/sources/tests/test_oracle.py -+++ b/cloudinit/sources/tests/test_oracle.py -@@ -173,6 +173,10 @@ class TestIsPlatformViable(test_helpers.CiTestCase): - m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')]) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestNetworkConfigFromOpcImds: - def test_no_secondary_nics_does_not_mutate_input(self, oracle_ds): - oracle_ds._vnics_data = [{}] -diff --git a/tests/integration_tests/bugs/test_lp1912844.py b/tests/integration_tests/bugs/test_lp1912844.py -new file mode 100644 -index 00000000..efafae50 ---- /dev/null -+++ b/tests/integration_tests/bugs/test_lp1912844.py -@@ -0,0 +1,103 @@ -+"""Integration test for LP: #1912844 -+ -+cloud-init should ignore OVS-internal interfaces when performing its own -+interface determination: these interfaces are handled fully by OVS, so -+cloud-init should never need to touch them. -+ -+This test is a semi-synthetic reproducer for the bug. It uses a similar -+network configuration, tweaked slightly to DHCP in a way that will succeed even -+on "failed" boots. The exact bug doesn't reproduce with the NoCloud -+datasource, because it runs at init-local time (whereas the MAAS datasource, -+from the report, runs only at init (network) time): this means that the -+networking code runs before OVS creates its interfaces (which happens after -+init-local but, of course, before networking is up), and so doesn't generate -+the traceback that they cause. We work around this by calling -+``get_interfaces_by_mac` directly in the test code. -+""" -+import pytest -+ -+from tests.integration_tests import random_mac_address -+ -+MAC_ADDRESS = random_mac_address() -+ -+NETWORK_CONFIG = """\ -+bonds: -+ bond0: -+ interfaces: -+ - enp5s0 -+ macaddress: {0} -+ mtu: 1500 -+bridges: -+ ovs-br: -+ interfaces: -+ - bond0 -+ macaddress: {0} -+ mtu: 1500 -+ openvswitch: {{}} -+ dhcp4: true -+ethernets: -+ enp5s0: -+ mtu: 1500 -+ set-name: enp5s0 -+ match: -+ macaddress: {0} -+version: 2 -+vlans: -+ ovs-br.100: -+ id: 100 -+ link: ovs-br -+ mtu: 1500 -+ ovs-br.200: -+ id: 200 -+ link: ovs-br -+ mtu: 1500 -+""".format(MAC_ADDRESS) -+ -+ -+SETUP_USER_DATA = """\ -+#cloud-config -+packages: -+- openvswitch-switch -+""" -+ -+ -+@pytest.fixture -+def ovs_enabled_session_cloud(session_cloud): -+ """A session_cloud wrapper, to use an OVS-enabled image for tests. -+ -+ This implementation is complicated by wanting to use ``session_cloud``s -+ snapshot cleanup/retention logic, to avoid having to reimplement that here. -+ """ -+ old_snapshot_id = session_cloud.snapshot_id -+ with session_cloud.launch( -+ user_data=SETUP_USER_DATA, -+ ) as instance: -+ instance.instance.clean() -+ session_cloud.snapshot_id = instance.snapshot() -+ -+ yield session_cloud -+ -+ try: -+ session_cloud.delete_snapshot() -+ finally: -+ session_cloud.snapshot_id = old_snapshot_id -+ -+ -+@pytest.mark.lxd_vm -+def test_get_interfaces_by_mac_doesnt_traceback(ovs_enabled_session_cloud): -+ """Launch our OVS-enabled image and confirm the bug doesn't reproduce.""" -+ launch_kwargs = { -+ "config_dict": { -+ "user.network-config": NETWORK_CONFIG, -+ "volatile.eth0.hwaddr": MAC_ADDRESS, -+ }, -+ } -+ with ovs_enabled_session_cloud.launch( -+ launch_kwargs=launch_kwargs, -+ ) as client: -+ result = client.execute( -+ "python3 -c" -+ "'from cloudinit.net import get_interfaces_by_mac;" -+ "get_interfaces_by_mac()'" -+ ) -+ assert result.ok -diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py -index 6f830cc6..2e2b7847 100644 ---- a/tests/unittests/test_datasource/test_configdrive.py -+++ b/tests/unittests/test_datasource/test_configdrive.py -@@ -494,6 +494,10 @@ class TestConfigDriveDataSource(CiTestCase): - self.assertEqual('config-disk (/dev/anything)', cfg_ds.subplatform) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestNetJson(CiTestCase): - def setUp(self): - super(TestNetJson, self).setUp() -@@ -654,6 +658,10 @@ class TestNetJson(CiTestCase): - self.assertEqual(out_data, conv_data) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestConvertNetworkData(CiTestCase): - - with_logs = True -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index c67b5fcc..14d3462f 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -2908,6 +2908,10 @@ iface eth1 inet dhcp - self.assertEqual(0, mock_settle.call_count) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestRhelSysConfigRendering(CiTestCase): - - with_logs = True -@@ -3592,6 +3596,10 @@ USERCTL=no - expected, self._render_and_read(network_config=v2data)) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestOpenSuseSysConfigRendering(CiTestCase): - - with_logs = True -@@ -5009,6 +5017,10 @@ class TestNetRenderers(CiTestCase): - self.assertTrue(result) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestGetInterfaces(CiTestCase): - _data = {'bonds': ['bond1'], - 'bridges': ['bridge1'], -@@ -5158,6 +5170,10 @@ class TestInterfaceHasOwnMac(CiTestCase): - self.assertFalse(interface_has_own_mac("eth0")) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestGetInterfacesByMac(CiTestCase): - _data = {'bonds': ['bond1'], - 'bridges': ['bridge1'], -@@ -5314,6 +5330,10 @@ class TestInterfacesSorting(CiTestCase): - ['enp0s3', 'enp0s8', 'enp0s13', 'enp1s2', 'enp2s0', 'enp2s3']) - - -+@mock.patch( -+ "cloudinit.net.is_openvswitch_internal_interface", -+ mock.Mock(return_value=False) -+) - class TestGetIBHwaddrsByInterface(CiTestCase): - - _ib_addr = '80:00:00:28:fe:80:00:00:00:00:00:00:00:11:22:03:00:33:44:56' --- -2.27.0 - diff --git a/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch b/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch deleted file mode 100644 index 0d474bc..0000000 --- a/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch +++ /dev/null @@ -1,87 +0,0 @@ -From bec5fb60ffae3d1137c7261e5571c2751c5dda25 Mon Sep 17 00:00:00 2001 -From: James Falcon <TheRealFalcon@users.noreply.github.com> -Date: Mon, 8 Mar 2021 14:09:47 -0600 -Subject: Fix requiring device-number on EC2 derivatives (#836) - -#342 (70dbccbb) introduced the ability to determine route-metrics based on -the `device-number` provided by the EC2 IMDS. Not all datasources that -subclass EC2 will have this attribute, so allow the old behavior if -`device-number` is not present. - -LP: #1917875 ---- - cloudinit/sources/DataSourceEc2.py | 3 +- - .../unittests/test_datasource/test_aliyun.py | 30 +++++++++++++++++++ - 2 files changed, 32 insertions(+), 1 deletion(-) - -diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py -index 1930a509..a2105dc7 100644 ---- a/cloudinit/sources/DataSourceEc2.py -+++ b/cloudinit/sources/DataSourceEc2.py -@@ -765,13 +765,14 @@ def convert_ec2_metadata_network_config( - netcfg['ethernets'][nic_name] = dev_config - return netcfg - # Apply network config for all nics and any secondary IPv4/v6 addresses -+ nic_idx = 0 - for mac, nic_name in sorted(macs_to_nics.items()): - nic_metadata = macs_metadata.get(mac) - if not nic_metadata: - continue # Not a physical nic represented in metadata - # device-number is zero-indexed, we want it 1-indexed for the - # multiplication on the following line -- nic_idx = int(nic_metadata['device-number']) + 1 -+ nic_idx = int(nic_metadata.get('device-number', nic_idx)) + 1 - dhcp_override = {'route-metric': nic_idx * 100} - dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override, - 'dhcp6': False, -diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py -index eb2828d5..cab1ac2b 100644 ---- a/tests/unittests/test_datasource/test_aliyun.py -+++ b/tests/unittests/test_datasource/test_aliyun.py -@@ -7,6 +7,7 @@ from unittest import mock - - from cloudinit import helpers - from cloudinit.sources import DataSourceAliYun as ay -+from cloudinit.sources.DataSourceEc2 import convert_ec2_metadata_network_config - from cloudinit.tests import helpers as test_helpers - - DEFAULT_METADATA = { -@@ -183,6 +184,35 @@ class TestAliYunDatasource(test_helpers.HttprettyTestCase): - self.assertEqual(ay.parse_public_keys(public_keys), - public_keys['key-pair-0']['openssh-key']) - -+ def test_route_metric_calculated_without_device_number(self): -+ """Test that route-metric code works without `device-number` -+ -+ `device-number` is part of EC2 metadata, but not supported on aliyun. -+ Attempting to access it will raise a KeyError. -+ -+ LP: #1917875 -+ """ -+ netcfg = convert_ec2_metadata_network_config( -+ {"interfaces": {"macs": { -+ "06:17:04:d7:26:09": { -+ "interface-id": "eni-e44ef49e", -+ }, -+ "06:17:04:d7:26:08": { -+ "interface-id": "eni-e44ef49f", -+ } -+ }}}, -+ macs_to_nics={ -+ '06:17:04:d7:26:09': 'eth0', -+ '06:17:04:d7:26:08': 'eth1', -+ } -+ ) -+ -+ met0 = netcfg['ethernets']['eth0']['dhcp4-overrides']['route-metric'] -+ met1 = netcfg['ethernets']['eth1']['dhcp4-overrides']['route-metric'] -+ -+ # route-metric numbers should be 100 apart -+ assert 100 == abs(met0 - met1) -+ - - class TestIsAliYun(test_helpers.CiTestCase): - ALIYUN_PRODUCT = 'Alibaba Cloud ECS' --- -2.27.0 - diff --git a/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch b/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch deleted file mode 100644 index 9dd373f..0000000 --- a/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch +++ /dev/null @@ -1,295 +0,0 @@ -From 2a2a5cdec0de0b96d503f9357c1641043574f90a Mon Sep 17 00:00:00 2001 -From: Thomas Stringer <thstring@microsoft.com> -Date: Wed, 3 Mar 2021 11:07:43 -0500 -Subject: [PATCH 1/7] Add flexibility to IMDS api-version (#793) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [1/7] 9aa42581c4ff175fb6f8f4a78d94cac9c9971062 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -Add flexibility to IMDS api-version by having both a desired IMDS -api-version and a minimum api-version. The desired api-version will -be used first, and if that fails it will fall back to the minimum -api-version. ---- - cloudinit/sources/DataSourceAzure.py | 113 ++++++++++++++---- - tests/unittests/test_datasource/test_azure.py | 42 ++++++- - 2 files changed, 129 insertions(+), 26 deletions(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index 553b5a7e..de1452ce 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -78,17 +78,15 @@ AGENT_SEED_DIR = '/var/lib/waagent' - # In the event where the IMDS primary server is not - # available, it takes 1s to fallback to the secondary one - IMDS_TIMEOUT_IN_SECONDS = 2 --IMDS_URL = "http://169.254.169.254/metadata/" --IMDS_VER = "2019-06-01" --IMDS_VER_PARAM = "api-version={}".format(IMDS_VER) -+IMDS_URL = "http://169.254.169.254/metadata" -+IMDS_VER_MIN = "2019-06-01" -+IMDS_VER_WANT = "2020-09-01" - - - class metadata_type(Enum): -- compute = "{}instance?{}".format(IMDS_URL, IMDS_VER_PARAM) -- network = "{}instance/network?{}".format(IMDS_URL, -- IMDS_VER_PARAM) -- reprovisiondata = "{}reprovisiondata?{}".format(IMDS_URL, -- IMDS_VER_PARAM) -+ compute = "{}/instance".format(IMDS_URL) -+ network = "{}/instance/network".format(IMDS_URL) -+ reprovisiondata = "{}/reprovisiondata".format(IMDS_URL) - - - PLATFORM_ENTROPY_SOURCE = "/sys/firmware/acpi/tables/OEM0" -@@ -349,6 +347,8 @@ class DataSourceAzure(sources.DataSource): - self.update_events['network'].add(EventType.BOOT) - self._ephemeral_dhcp_ctx = None - -+ self.failed_desired_api_version = False -+ - def __str__(self): - root = sources.DataSource.__str__(self) - return "%s [seed=%s]" % (root, self.seed) -@@ -520,8 +520,10 @@ class DataSourceAzure(sources.DataSource): - self._wait_for_all_nics_ready() - ret = self._reprovision() - -- imds_md = get_metadata_from_imds( -- self.fallback_interface, retries=10) -+ imds_md = self.get_imds_data_with_api_fallback( -+ self.fallback_interface, -+ retries=10 -+ ) - (md, userdata_raw, cfg, files) = ret - self.seed = cdev - crawled_data.update({ -@@ -652,6 +654,57 @@ class DataSourceAzure(sources.DataSource): - self.ds_cfg['data_dir'], crawled_data['files'], dirmode=0o700) - return True - -+ @azure_ds_telemetry_reporter -+ def get_imds_data_with_api_fallback( -+ self, -+ fallback_nic, -+ retries, -+ md_type=metadata_type.compute): -+ """ -+ Wrapper for get_metadata_from_imds so that we can have flexibility -+ in which IMDS api-version we use. If a particular instance of IMDS -+ does not have the api version that is desired, we want to make -+ this fault tolerant and fall back to a good known minimum api -+ version. -+ """ -+ -+ if not self.failed_desired_api_version: -+ for _ in range(retries): -+ try: -+ LOG.info( -+ "Attempting IMDS api-version: %s", -+ IMDS_VER_WANT -+ ) -+ return get_metadata_from_imds( -+ fallback_nic=fallback_nic, -+ retries=0, -+ md_type=md_type, -+ api_version=IMDS_VER_WANT -+ ) -+ except UrlError as err: -+ LOG.info( -+ "UrlError with IMDS api-version: %s", -+ IMDS_VER_WANT -+ ) -+ if err.code == 400: -+ log_msg = "Fall back to IMDS api-version: {}".format( -+ IMDS_VER_MIN -+ ) -+ report_diagnostic_event( -+ log_msg, -+ logger_func=LOG.info -+ ) -+ self.failed_desired_api_version = True -+ break -+ -+ LOG.info("Using IMDS api-version: %s", IMDS_VER_MIN) -+ return get_metadata_from_imds( -+ fallback_nic=fallback_nic, -+ retries=retries, -+ md_type=md_type, -+ api_version=IMDS_VER_MIN -+ ) -+ - def device_name_to_device(self, name): - return self.ds_cfg['disk_aliases'].get(name) - -@@ -880,10 +933,11 @@ class DataSourceAzure(sources.DataSource): - # primary nic is being attached first helps here. Otherwise each nic - # could add several seconds of delay. - try: -- imds_md = get_metadata_from_imds( -+ imds_md = self.get_imds_data_with_api_fallback( - ifname, - 5, -- metadata_type.network) -+ metadata_type.network -+ ) - except Exception as e: - LOG.warning( - "Failed to get network metadata using nic %s. Attempt to " -@@ -1017,7 +1071,10 @@ class DataSourceAzure(sources.DataSource): - def _poll_imds(self): - """Poll IMDS for the new provisioning data until we get a valid - response. Then return the returned JSON object.""" -- url = metadata_type.reprovisiondata.value -+ url = "{}?api-version={}".format( -+ metadata_type.reprovisiondata.value, -+ IMDS_VER_MIN -+ ) - headers = {"Metadata": "true"} - nl_sock = None - report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE)) -@@ -2059,7 +2116,8 @@ def _generate_network_config_from_fallback_config() -> dict: - @azure_ds_telemetry_reporter - def get_metadata_from_imds(fallback_nic, - retries, -- md_type=metadata_type.compute): -+ md_type=metadata_type.compute, -+ api_version=IMDS_VER_MIN): - """Query Azure's instance metadata service, returning a dictionary. - - If network is not up, setup ephemeral dhcp on fallback_nic to talk to the -@@ -2069,13 +2127,16 @@ def get_metadata_from_imds(fallback_nic, - @param fallback_nic: String. The name of the nic which requires active - network in order to query IMDS. - @param retries: The number of retries of the IMDS_URL. -+ @param md_type: Metadata type for IMDS request. -+ @param api_version: IMDS api-version to use in the request. - - @return: A dict of instance metadata containing compute and network - info. - """ - kwargs = {'logfunc': LOG.debug, - 'msg': 'Crawl of Azure Instance Metadata Service (IMDS)', -- 'func': _get_metadata_from_imds, 'args': (retries, md_type,)} -+ 'func': _get_metadata_from_imds, -+ 'args': (retries, md_type, api_version,)} - if net.is_up(fallback_nic): - return util.log_time(**kwargs) - else: -@@ -2091,20 +2152,26 @@ def get_metadata_from_imds(fallback_nic, - - - @azure_ds_telemetry_reporter --def _get_metadata_from_imds(retries, md_type=metadata_type.compute): -- -- url = md_type.value -+def _get_metadata_from_imds( -+ retries, -+ md_type=metadata_type.compute, -+ api_version=IMDS_VER_MIN): -+ url = "{}?api-version={}".format(md_type.value, api_version) - headers = {"Metadata": "true"} - try: - response = readurl( - url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers, - retries=retries, exception_cb=retry_on_url_exc) - except Exception as e: -- report_diagnostic_event( -- 'Ignoring IMDS instance metadata. ' -- 'Get metadata from IMDS failed: %s' % e, -- logger_func=LOG.warning) -- return {} -+ # pylint:disable=no-member -+ if isinstance(e, UrlError) and e.code == 400: -+ raise -+ else: -+ report_diagnostic_event( -+ 'Ignoring IMDS instance metadata. ' -+ 'Get metadata from IMDS failed: %s' % e, -+ logger_func=LOG.warning) -+ return {} - try: - from json.decoder import JSONDecodeError - json_decode_error = JSONDecodeError -diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py -index f597c723..dedebeb1 100644 ---- a/tests/unittests/test_datasource/test_azure.py -+++ b/tests/unittests/test_datasource/test_azure.py -@@ -408,7 +408,9 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): - - def setUp(self): - super(TestGetMetadataFromIMDS, self).setUp() -- self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2019-06-01" -+ self.network_md_url = "{}/instance?api-version=2019-06-01".format( -+ dsaz.IMDS_URL -+ ) - - @mock.patch(MOCKPATH + 'readurl') - @mock.patch(MOCKPATH + 'EphemeralDHCPv4', autospec=True) -@@ -518,7 +520,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): - """Return empty dict when IMDS network metadata is absent.""" - httpretty.register_uri( - httpretty.GET, -- dsaz.IMDS_URL + 'instance?api-version=2017-12-01', -+ dsaz.IMDS_URL + '/instance?api-version=2017-12-01', - body={}, status=404) - - m_net_is_up.return_value = True # skips dhcp -@@ -1877,6 +1879,40 @@ scbus-1 on xpt0 bus 0 - ssh_keys = dsrc.get_public_ssh_keys() - self.assertEqual(ssh_keys, ['key2']) - -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_imds_api_version_wanted_nonexistent( -+ self, -+ m_get_metadata_from_imds): -+ def get_metadata_from_imds_side_eff(*args, **kwargs): -+ if kwargs['api_version'] == dsaz.IMDS_VER_WANT: -+ raise url_helper.UrlError("No IMDS version", code=400) -+ return NETWORK_METADATA -+ m_get_metadata_from_imds.side_effect = get_metadata_from_imds_side_eff -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ dsrc = self._get_ds(data) -+ dsrc.get_data() -+ self.assertIsNotNone(dsrc.metadata) -+ self.assertTrue(dsrc.failed_desired_api_version) -+ -+ @mock.patch( -+ MOCKPATH + 'get_metadata_from_imds', return_value=NETWORK_METADATA) -+ def test_imds_api_version_wanted_exists(self, m_get_metadata_from_imds): -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ dsrc = self._get_ds(data) -+ dsrc.get_data() -+ self.assertIsNotNone(dsrc.metadata) -+ self.assertFalse(dsrc.failed_desired_api_version) -+ - - class TestAzureBounce(CiTestCase): - -@@ -2657,7 +2693,7 @@ class TestPreprovisioningHotAttachNics(CiTestCase): - @mock.patch(MOCKPATH + 'DataSourceAzure.wait_for_link_up') - @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_attach_event') - @mock.patch('cloudinit.sources.net.find_fallback_nic') -- @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback') - @mock.patch(MOCKPATH + 'EphemeralDHCPv4') - @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach') - @mock.patch('os.path.isfile') --- -2.27.0 - 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..aad448a --- /dev/null +++ b/SOURCES/ci-Add-native-NetworkManager-support-1224.patch @@ -0,0 +1,2300 @@ +From 0d93e53fd05c44b62e3456b7580c9de8135e6b5a Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Mon, 2 May 2022 14:21:24 +0200 +Subject: [PATCH 1/4] Add native NetworkManager support (#1224) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 57: Add native NetworkManager support (#1224) +RH-Commit: [1/2] 56b9ed40840a4930c421c2749e8aa385097bef93 +RH-Bugzilla: 2059872 +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Jon Maloy <jmaloy@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> + +commit feda344e6cf9d37b09bc13cf333a717d1654c26c +Author: Lubomir Rintel <lkundrak@v3.sk> +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 <eesposit@redhat.com> +--- + 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 <lkundrak@v3.sk> ++# 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<n> or route<n>, ensuring ++ the appropriate value gets used for <n>. ++ """ ++ ++ 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<n> property. ++ """ ++ ++ value = subnet["address"] + "/" + str(subnet["prefix"]) ++ self._add_numbered(family, "address", value) ++ ++ def _add_route(self, family, route): ++ """ ++ Adds a ipv[46].route<n> 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 362e8d19..c3b0c795 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.""" + +@@ -1031,8 +1011,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 +@@ -1071,14 +1049,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: +@@ -1095,10 +1068,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.35.3 + 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..7346183 --- /dev/null +++ b/SOURCES/ci-Align-rhel-custom-files-with-upstream-1431.patch @@ -0,0 +1,257 @@ +From 5c99ba05086b1ec83ce7e0c64edb4add4b47d923 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Thu, 19 May 2022 11:14:39 +0200 +Subject: [PATCH 3/4] Align rhel custom files with upstream (#1431) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 65: Align rhel custom files with upstream (#1431) +RH-Commit: [1/2] 5d9067175688b1006472a477b0916b81c73d5e07 +RH-Bugzilla: 2082071 +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> + +commit 9624758f91b61f4711e8d7b5c83075b5d23e0c43 +Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +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 <eesposit@redhat.com> + + RHBZ: 2082071 + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + 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.35.3 + diff --git a/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch b/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch deleted file mode 100644 index de27366..0000000 --- a/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch +++ /dev/null @@ -1,397 +0,0 @@ -From 3ec4ddbc595c5fe781b3dc501631d23569849818 Mon Sep 17 00:00:00 2001 -From: Thomas Stringer <thstring@microsoft.com> -Date: Mon, 26 Apr 2021 09:41:38 -0400 -Subject: [PATCH 5/7] Azure: Retrieve username and hostname from IMDS (#865) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [5/7] 6fab7ef28c7fd340bda4f82dbf828f10716cb3f1 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -This change allows us to retrieve the username and hostname from -IMDS instead of having to rely on the mounted OVF. ---- - cloudinit/sources/DataSourceAzure.py | 149 ++++++++++++++---- - tests/unittests/test_datasource/test_azure.py | 87 +++++++++- - 2 files changed, 205 insertions(+), 31 deletions(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index 39e67c4f..6d7954ee 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -5,6 +5,7 @@ - # This file is part of cloud-init. See LICENSE file for license information. - - import base64 -+from collections import namedtuple - import contextlib - import crypt - from functools import partial -@@ -25,6 +26,7 @@ from cloudinit.net import device_driver - from cloudinit.net.dhcp import EphemeralDHCPv4 - from cloudinit import sources - from cloudinit.sources.helpers import netlink -+from cloudinit import ssh_util - from cloudinit import subp - from cloudinit.url_helper import UrlError, readurl, retry_on_url_exc - from cloudinit import util -@@ -80,7 +82,12 @@ AGENT_SEED_DIR = '/var/lib/waagent' - IMDS_TIMEOUT_IN_SECONDS = 2 - IMDS_URL = "http://169.254.169.254/metadata" - IMDS_VER_MIN = "2019-06-01" --IMDS_VER_WANT = "2020-09-01" -+IMDS_VER_WANT = "2020-10-01" -+ -+ -+# This holds SSH key data including if the source was -+# from IMDS, as well as the SSH key data itself. -+SSHKeys = namedtuple("SSHKeys", ("keys_from_imds", "ssh_keys")) - - - class metadata_type(Enum): -@@ -391,6 +398,8 @@ class DataSourceAzure(sources.DataSource): - """Return the subplatform metadata source details.""" - if self.seed.startswith('/dev'): - subplatform_type = 'config-disk' -+ elif self.seed.lower() == 'imds': -+ subplatform_type = 'imds' - else: - subplatform_type = 'seed-dir' - return '%s (%s)' % (subplatform_type, self.seed) -@@ -433,9 +442,11 @@ class DataSourceAzure(sources.DataSource): - - found = None - reprovision = False -+ ovf_is_accessible = True - reprovision_after_nic_attach = False - for cdev in candidates: - try: -+ LOG.debug("cdev: %s", cdev) - if cdev == "IMDS": - ret = None - reprovision = True -@@ -462,8 +473,18 @@ class DataSourceAzure(sources.DataSource): - raise sources.InvalidMetaDataException(msg) - except util.MountFailedError: - report_diagnostic_event( -- '%s was not mountable' % cdev, logger_func=LOG.warning) -- continue -+ '%s was not mountable' % cdev, logger_func=LOG.debug) -+ cdev = 'IMDS' -+ ovf_is_accessible = False -+ empty_md = {'local-hostname': ''} -+ empty_cfg = dict( -+ system_info=dict( -+ default_user=dict( -+ name='' -+ ) -+ ) -+ ) -+ ret = (empty_md, '', empty_cfg, {}) - - report_diagnostic_event("Found provisioning metadata in %s" % cdev, - logger_func=LOG.debug) -@@ -490,6 +511,10 @@ class DataSourceAzure(sources.DataSource): - self.fallback_interface, - retries=10 - ) -+ if not imds_md and not ovf_is_accessible: -+ msg = 'No OVF or IMDS available' -+ report_diagnostic_event(msg) -+ raise sources.InvalidMetaDataException(msg) - (md, userdata_raw, cfg, files) = ret - self.seed = cdev - crawled_data.update({ -@@ -498,6 +523,21 @@ class DataSourceAzure(sources.DataSource): - 'metadata': util.mergemanydict( - [md, {'imds': imds_md}]), - 'userdata_raw': userdata_raw}) -+ imds_username = _username_from_imds(imds_md) -+ imds_hostname = _hostname_from_imds(imds_md) -+ imds_disable_password = _disable_password_from_imds(imds_md) -+ if imds_username: -+ LOG.debug('Username retrieved from IMDS: %s', imds_username) -+ cfg['system_info']['default_user']['name'] = imds_username -+ if imds_hostname: -+ LOG.debug('Hostname retrieved from IMDS: %s', imds_hostname) -+ crawled_data['metadata']['local-hostname'] = imds_hostname -+ if imds_disable_password: -+ LOG.debug( -+ 'Disable password retrieved from IMDS: %s', -+ imds_disable_password -+ ) -+ crawled_data['metadata']['disable_password'] = imds_disable_password # noqa: E501 - found = cdev - - report_diagnostic_event( -@@ -676,6 +716,13 @@ class DataSourceAzure(sources.DataSource): - - @azure_ds_telemetry_reporter - def get_public_ssh_keys(self): -+ """ -+ Retrieve public SSH keys. -+ """ -+ -+ return self._get_public_ssh_keys_and_source().ssh_keys -+ -+ def _get_public_ssh_keys_and_source(self): - """ - Try to get the ssh keys from IMDS first, and if that fails - (i.e. IMDS is unavailable) then fallback to getting the ssh -@@ -685,30 +732,50 @@ class DataSourceAzure(sources.DataSource): - advantage, so this is a strong preference. But we must keep - OVF as a second option for environments that don't have IMDS. - """ -+ - LOG.debug('Retrieving public SSH keys') - ssh_keys = [] -+ keys_from_imds = True -+ LOG.debug('Attempting to get SSH keys from IMDS') - try: -- raise KeyError( -- "Not using public SSH keys from IMDS" -- ) -- # pylint:disable=unreachable - ssh_keys = [ - public_key['keyData'] - for public_key - in self.metadata['imds']['compute']['publicKeys'] - ] -- LOG.debug('Retrieved SSH keys from IMDS') -+ for key in ssh_keys: -+ if not _key_is_openssh_formatted(key=key): -+ keys_from_imds = False -+ break -+ -+ if not keys_from_imds: -+ log_msg = 'Keys not in OpenSSH format, using OVF' -+ else: -+ log_msg = 'Retrieved {} keys from IMDS'.format( -+ len(ssh_keys) -+ if ssh_keys is not None -+ else 0 -+ ) - except KeyError: - log_msg = 'Unable to get keys from IMDS, falling back to OVF' -+ keys_from_imds = False -+ finally: - report_diagnostic_event(log_msg, logger_func=LOG.debug) -+ -+ if not keys_from_imds: -+ LOG.debug('Attempting to get SSH keys from OVF') - try: - ssh_keys = self.metadata['public-keys'] -- LOG.debug('Retrieved keys from OVF') -+ log_msg = 'Retrieved {} keys from OVF'.format(len(ssh_keys)) - except KeyError: - log_msg = 'No keys available from OVF' -+ finally: - report_diagnostic_event(log_msg, logger_func=LOG.debug) - -- return ssh_keys -+ return SSHKeys( -+ keys_from_imds=keys_from_imds, -+ ssh_keys=ssh_keys -+ ) - - def get_config_obj(self): - return self.cfg -@@ -1325,30 +1392,21 @@ class DataSourceAzure(sources.DataSource): - self.bounce_network_with_azure_hostname() - - pubkey_info = None -- try: -- raise KeyError( -- "Not using public SSH keys from IMDS" -- ) -- # pylint:disable=unreachable -- public_keys = self.metadata['imds']['compute']['publicKeys'] -- LOG.debug( -- 'Successfully retrieved %s key(s) from IMDS', -- len(public_keys) -- if public_keys is not None -+ ssh_keys_and_source = self._get_public_ssh_keys_and_source() -+ -+ if not ssh_keys_and_source.keys_from_imds: -+ pubkey_info = self.cfg.get('_pubkeys', None) -+ log_msg = 'Retrieved {} fingerprints from OVF'.format( -+ len(pubkey_info) -+ if pubkey_info is not None - else 0 - ) -- except KeyError: -- LOG.debug( -- 'Unable to retrieve SSH keys from IMDS during ' -- 'negotiation, falling back to OVF' -- ) -- pubkey_info = self.cfg.get('_pubkeys', None) -+ report_diagnostic_event(log_msg, logger_func=LOG.debug) - - metadata_func = partial(get_metadata_from_fabric, - fallback_lease_file=self. - dhclient_lease_file, -- pubkey_info=pubkey_info, -- iso_dev=self.iso_dev) -+ pubkey_info=pubkey_info) - - LOG.debug("negotiating with fabric via agent command %s", - self.ds_cfg['agent_command']) -@@ -1404,6 +1462,41 @@ class DataSourceAzure(sources.DataSource): - return self.metadata.get('imds', {}).get('compute', {}).get('location') - - -+def _username_from_imds(imds_data): -+ try: -+ return imds_data['compute']['osProfile']['adminUsername'] -+ except KeyError: -+ return None -+ -+ -+def _hostname_from_imds(imds_data): -+ try: -+ return imds_data['compute']['osProfile']['computerName'] -+ except KeyError: -+ return None -+ -+ -+def _disable_password_from_imds(imds_data): -+ try: -+ return imds_data['compute']['osProfile']['disablePasswordAuthentication'] == 'true' # noqa: E501 -+ except KeyError: -+ return None -+ -+ -+def _key_is_openssh_formatted(key): -+ """ -+ Validate whether or not the key is OpenSSH-formatted. -+ """ -+ -+ parser = ssh_util.AuthKeyLineParser() -+ try: -+ akl = parser.parse(key) -+ except TypeError: -+ return False -+ -+ return akl.keytype is not None -+ -+ - def _partitions_on_device(devpath, maxnum=16): - # return a list of tuples (ptnum, path) for each part on devpath - for suff in ("-part", "p", ""): -diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py -index 320fa857..d9817d84 100644 ---- a/tests/unittests/test_datasource/test_azure.py -+++ b/tests/unittests/test_datasource/test_azure.py -@@ -108,7 +108,7 @@ NETWORK_METADATA = { - "zone": "", - "publicKeys": [ - { -- "keyData": "key1", -+ "keyData": "ssh-rsa key1", - "path": "path1" - } - ] -@@ -1761,8 +1761,29 @@ scbus-1 on xpt0 bus 0 - dsrc.get_data() - dsrc.setup(True) - ssh_keys = dsrc.get_public_ssh_keys() -- # Temporarily alter this test so that SSH public keys -- # from IMDS are *not* going to be in use to fix a regression. -+ self.assertEqual(ssh_keys, ["ssh-rsa key1"]) -+ self.assertEqual(m_parse_certificates.call_count, 0) -+ -+ @mock.patch( -+ 'cloudinit.sources.helpers.azure.OpenSSLManager.parse_certificates') -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_get_public_ssh_keys_with_no_openssh_format( -+ self, -+ m_get_metadata_from_imds, -+ m_parse_certificates): -+ imds_data = copy.deepcopy(NETWORK_METADATA) -+ imds_data['compute']['publicKeys'][0]['keyData'] = 'no-openssh-format' -+ m_get_metadata_from_imds.return_value = imds_data -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ dsrc = self._get_ds(data) -+ dsrc.get_data() -+ dsrc.setup(True) -+ ssh_keys = dsrc.get_public_ssh_keys() - self.assertEqual(ssh_keys, []) - self.assertEqual(m_parse_certificates.call_count, 0) - -@@ -1818,6 +1839,66 @@ scbus-1 on xpt0 bus 0 - self.assertIsNotNone(dsrc.metadata) - self.assertFalse(dsrc.failed_desired_api_version) - -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_hostname_from_imds(self, m_get_metadata_from_imds): -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA) -+ imds_data_with_os_profile["compute"]["osProfile"] = dict( -+ adminUsername="username1", -+ computerName="hostname1", -+ disablePasswordAuthentication="true" -+ ) -+ m_get_metadata_from_imds.return_value = imds_data_with_os_profile -+ dsrc = self._get_ds(data) -+ dsrc.get_data() -+ self.assertEqual(dsrc.metadata["local-hostname"], "hostname1") -+ -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_username_from_imds(self, m_get_metadata_from_imds): -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA) -+ imds_data_with_os_profile["compute"]["osProfile"] = dict( -+ adminUsername="username1", -+ computerName="hostname1", -+ disablePasswordAuthentication="true" -+ ) -+ m_get_metadata_from_imds.return_value = imds_data_with_os_profile -+ dsrc = self._get_ds(data) -+ dsrc.get_data() -+ self.assertEqual( -+ dsrc.cfg["system_info"]["default_user"]["name"], -+ "username1" -+ ) -+ -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_disable_password_from_imds(self, m_get_metadata_from_imds): -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA) -+ imds_data_with_os_profile["compute"]["osProfile"] = dict( -+ adminUsername="username1", -+ computerName="hostname1", -+ disablePasswordAuthentication="true" -+ ) -+ m_get_metadata_from_imds.return_value = imds_data_with_os_profile -+ dsrc = self._get_ds(data) -+ dsrc.get_data() -+ self.assertTrue(dsrc.metadata["disable_password"]) -+ - - class TestAzureBounce(CiTestCase): - --- -2.27.0 - diff --git a/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch b/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch deleted file mode 100644 index efc9fc2..0000000 --- a/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch +++ /dev/null @@ -1,315 +0,0 @@ -From ca5b83cee7b45bf56eec258db739cb5fe51b3231 Mon Sep 17 00:00:00 2001 -From: aswinrajamannar <39812128+aswinrajamannar@users.noreply.github.com> -Date: Mon, 26 Apr 2021 07:28:39 -0700 -Subject: [PATCH 6/7] Azure: Retry net metadata during nic attach for - non-timeout errs (#878) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [6/7] 4e6e44f017d5ffcb72ac8959a94f80c71fef9560 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -When network interfaces are hot-attached to the VM, attempting to get -network metadata might return 410 (or 500, 503 etc) because the info -is not yet available. In those cases, we retry getting the metadata -before giving up. The only case where we can move on to wait for more -nic attach events is if the call times out despite retries, which -means the interface is not likely a primary interface, and we should -try for more nic attach events. ---- - cloudinit/sources/DataSourceAzure.py | 65 +++++++++++-- - tests/unittests/test_datasource/test_azure.py | 95 ++++++++++++++++--- - 2 files changed, 140 insertions(+), 20 deletions(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index 6d7954ee..d0be6d84 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -17,6 +17,7 @@ from time import sleep - from xml.dom import minidom - import xml.etree.ElementTree as ET - from enum import Enum -+import requests - - from cloudinit import dmi - from cloudinit import log as logging -@@ -665,7 +666,9 @@ class DataSourceAzure(sources.DataSource): - self, - fallback_nic, - retries, -- md_type=metadata_type.compute): -+ md_type=metadata_type.compute, -+ exc_cb=retry_on_url_exc, -+ infinite=False): - """ - Wrapper for get_metadata_from_imds so that we can have flexibility - in which IMDS api-version we use. If a particular instance of IMDS -@@ -685,7 +688,8 @@ class DataSourceAzure(sources.DataSource): - fallback_nic=fallback_nic, - retries=0, - md_type=md_type, -- api_version=IMDS_VER_WANT -+ api_version=IMDS_VER_WANT, -+ exc_cb=exc_cb - ) - except UrlError as err: - LOG.info( -@@ -708,7 +712,9 @@ class DataSourceAzure(sources.DataSource): - fallback_nic=fallback_nic, - retries=retries, - md_type=md_type, -- api_version=IMDS_VER_MIN -+ api_version=IMDS_VER_MIN, -+ exc_cb=exc_cb, -+ infinite=infinite - ) - - def device_name_to_device(self, name): -@@ -938,6 +944,9 @@ class DataSourceAzure(sources.DataSource): - is_primary = False - expected_nic_count = -1 - imds_md = None -+ metadata_poll_count = 0 -+ metadata_logging_threshold = 1 -+ metadata_timeout_count = 0 - - # For now, only a VM's primary NIC can contact IMDS and WireServer. If - # DHCP fails for a NIC, we have no mechanism to determine if the NIC is -@@ -962,14 +971,48 @@ class DataSourceAzure(sources.DataSource): - % (ifname, e), logger_func=LOG.error) - raise - -+ # Retry polling network metadata for a limited duration only when the -+ # calls fail due to timeout. This is because the platform drops packets -+ # going towards IMDS when it is not a primary nic. If the calls fail -+ # due to other issues like 410, 503 etc, then it means we are primary -+ # but IMDS service is unavailable at the moment. Retry indefinitely in -+ # those cases since we cannot move on without the network metadata. -+ def network_metadata_exc_cb(msg, exc): -+ nonlocal metadata_timeout_count, metadata_poll_count -+ nonlocal metadata_logging_threshold -+ -+ metadata_poll_count = metadata_poll_count + 1 -+ -+ # Log when needed but back off exponentially to avoid exploding -+ # the log file. -+ if metadata_poll_count >= metadata_logging_threshold: -+ metadata_logging_threshold *= 2 -+ report_diagnostic_event( -+ "Ran into exception when attempting to reach %s " -+ "after %d polls." % (msg, metadata_poll_count), -+ logger_func=LOG.error) -+ -+ if isinstance(exc, UrlError): -+ report_diagnostic_event("poll IMDS with %s failed. " -+ "Exception: %s and code: %s" % -+ (msg, exc.cause, exc.code), -+ logger_func=LOG.error) -+ -+ if exc.cause and isinstance(exc.cause, requests.Timeout): -+ metadata_timeout_count = metadata_timeout_count + 1 -+ return (metadata_timeout_count <= 10) -+ return True -+ - # Primary nic detection will be optimized in the future. The fact that - # primary nic is being attached first helps here. Otherwise each nic - # could add several seconds of delay. - try: - imds_md = self.get_imds_data_with_api_fallback( - ifname, -- 5, -- metadata_type.network -+ 0, -+ metadata_type.network, -+ network_metadata_exc_cb, -+ True - ) - except Exception as e: - LOG.warning( -@@ -2139,7 +2182,9 @@ def _generate_network_config_from_fallback_config() -> dict: - def get_metadata_from_imds(fallback_nic, - retries, - md_type=metadata_type.compute, -- api_version=IMDS_VER_MIN): -+ api_version=IMDS_VER_MIN, -+ exc_cb=retry_on_url_exc, -+ infinite=False): - """Query Azure's instance metadata service, returning a dictionary. - - If network is not up, setup ephemeral dhcp on fallback_nic to talk to the -@@ -2158,7 +2203,7 @@ def get_metadata_from_imds(fallback_nic, - kwargs = {'logfunc': LOG.debug, - 'msg': 'Crawl of Azure Instance Metadata Service (IMDS)', - 'func': _get_metadata_from_imds, -- 'args': (retries, md_type, api_version,)} -+ 'args': (retries, exc_cb, md_type, api_version, infinite)} - if net.is_up(fallback_nic): - return util.log_time(**kwargs) - else: -@@ -2176,14 +2221,16 @@ def get_metadata_from_imds(fallback_nic, - @azure_ds_telemetry_reporter - def _get_metadata_from_imds( - retries, -+ exc_cb, - md_type=metadata_type.compute, -- api_version=IMDS_VER_MIN): -+ api_version=IMDS_VER_MIN, -+ infinite=False): - url = "{}?api-version={}".format(md_type.value, api_version) - headers = {"Metadata": "true"} - try: - response = readurl( - url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers, -- retries=retries, exception_cb=retry_on_url_exc) -+ retries=retries, exception_cb=exc_cb, infinite=infinite) - except Exception as e: - # pylint:disable=no-member - if isinstance(e, UrlError) and e.code == 400: -diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py -index d9817d84..c4a8e08d 100644 ---- a/tests/unittests/test_datasource/test_azure.py -+++ b/tests/unittests/test_datasource/test_azure.py -@@ -448,7 +448,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): - "http://169.254.169.254/metadata/instance?api-version=" - "2019-06-01", exception_cb=mock.ANY, - headers=mock.ANY, retries=mock.ANY, -- timeout=mock.ANY) -+ timeout=mock.ANY, infinite=False) - - @mock.patch(MOCKPATH + 'readurl', autospec=True) - @mock.patch(MOCKPATH + 'EphemeralDHCPv4') -@@ -467,7 +467,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): - "http://169.254.169.254/metadata/instance/network?api-version=" - "2019-06-01", exception_cb=mock.ANY, - headers=mock.ANY, retries=mock.ANY, -- timeout=mock.ANY) -+ timeout=mock.ANY, infinite=False) - - @mock.patch(MOCKPATH + 'readurl', autospec=True) - @mock.patch(MOCKPATH + 'EphemeralDHCPv4') -@@ -486,7 +486,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): - "http://169.254.169.254/metadata/instance?api-version=" - "2019-06-01", exception_cb=mock.ANY, - headers=mock.ANY, retries=mock.ANY, -- timeout=mock.ANY) -+ timeout=mock.ANY, infinite=False) - - @mock.patch(MOCKPATH + 'readurl', autospec=True) - @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting', autospec=True) -@@ -511,7 +511,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): - m_readurl.assert_called_with( - self.network_md_url, exception_cb=mock.ANY, - headers={'Metadata': 'true'}, retries=2, -- timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS) -+ timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS, infinite=False) - - @mock.patch('cloudinit.url_helper.time.sleep') - @mock.patch(MOCKPATH + 'net.is_up', autospec=True) -@@ -2694,15 +2694,22 @@ class TestPreprovisioningHotAttachNics(CiTestCase): - - def nic_attach_ret(nl_sock, nics_found): - nonlocal m_attach_call_count -- if m_attach_call_count == 0: -- m_attach_call_count = m_attach_call_count + 1 -+ m_attach_call_count = m_attach_call_count + 1 -+ if m_attach_call_count == 1: - return "eth0" -- return "eth1" -+ elif m_attach_call_count == 2: -+ return "eth1" -+ raise RuntimeError("Must have found primary nic by now.") -+ -+ # Simulate two NICs by adding the same one twice. -+ md = { -+ "interface": [ -+ IMDS_NETWORK_METADATA['interface'][0], -+ IMDS_NETWORK_METADATA['interface'][0] -+ ] -+ } - -- def network_metadata_ret(ifname, retries, type): -- # Simulate two NICs by adding the same one twice. -- md = IMDS_NETWORK_METADATA -- md['interface'].append(md['interface'][0]) -+ def network_metadata_ret(ifname, retries, type, exc_cb, infinite): - if ifname == "eth0": - return md - raise requests.Timeout('Fake connection timeout') -@@ -2724,6 +2731,72 @@ class TestPreprovisioningHotAttachNics(CiTestCase): - self.assertEqual(1, m_imds.call_count) - self.assertEqual(2, m_link_up.call_count) - -+ @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback') -+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4') -+ def test_check_if_nic_is_primary_retries_on_failures( -+ self, m_dhcpv4, m_imds): -+ """Retry polling for network metadata on all failures except timeout""" -+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths) -+ lease = { -+ 'interface': 'eth9', 'fixed-address': '192.168.2.9', -+ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0', -+ 'unknown-245': '624c3620'} -+ -+ eth0Retries = [] -+ eth1Retries = [] -+ # Simulate two NICs by adding the same one twice. -+ md = { -+ "interface": [ -+ IMDS_NETWORK_METADATA['interface'][0], -+ IMDS_NETWORK_METADATA['interface'][0] -+ ] -+ } -+ -+ def network_metadata_ret(ifname, retries, type, exc_cb, infinite): -+ nonlocal eth0Retries, eth1Retries -+ -+ # Simulate readurl functionality with retries and -+ # exception callbacks so that the callback logic can be -+ # validated. -+ if ifname == "eth0": -+ cause = requests.HTTPError() -+ for _ in range(0, 15): -+ error = url_helper.UrlError(cause=cause, code=410) -+ eth0Retries.append(exc_cb("No goal state.", error)) -+ else: -+ cause = requests.Timeout('Fake connection timeout') -+ for _ in range(0, 10): -+ error = url_helper.UrlError(cause=cause) -+ eth1Retries.append(exc_cb("Connection timeout", error)) -+ # Should stop retrying after 10 retries -+ eth1Retries.append(exc_cb("Connection timeout", error)) -+ raise cause -+ return md -+ -+ m_imds.side_effect = network_metadata_ret -+ -+ dhcp_ctx = mock.MagicMock(lease=lease) -+ dhcp_ctx.obtain_lease.return_value = lease -+ m_dhcpv4.return_value = dhcp_ctx -+ -+ is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth0") -+ self.assertEqual(True, is_primary) -+ self.assertEqual(2, expected_nic_count) -+ -+ # All Eth0 errors are non-timeout errors. So we should have been -+ # retrying indefinitely until success. -+ for i in eth0Retries: -+ self.assertTrue(i) -+ -+ is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth1") -+ self.assertEqual(False, is_primary) -+ -+ # All Eth1 errors are timeout errors. Retry happens for a max of 10 and -+ # then we should have moved on assuming it is not the primary nic. -+ for i in range(0, 10): -+ self.assertTrue(eth1Retries[i]) -+ self.assertFalse(eth1Retries[10]) -+ - @mock.patch('cloudinit.distros.networking.LinuxNetworking.try_set_link_up') - def test_wait_for_link_up_returns_if_already_up( - self, m_is_link_up): --- -2.27.0 - diff --git a/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch b/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch deleted file mode 100644 index d4e7e37..0000000 --- a/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch +++ /dev/null @@ -1,129 +0,0 @@ -From c0df7233fa99d4191b5d4142e209e7465d8db5f6 Mon Sep 17 00:00:00 2001 -From: Anh Vo <anhvo@microsoft.com> -Date: Tue, 27 Apr 2021 13:40:59 -0400 -Subject: [PATCH 7/7] Azure: adding support for consuming userdata from IMDS - (#884) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [7/7] 32f840412da1a0f49b9ab5ba1d6f1bcb1bfacc16 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> ---- - cloudinit/sources/DataSourceAzure.py | 23 ++++++++- - tests/unittests/test_datasource/test_azure.py | 50 +++++++++++++++++++ - 2 files changed, 72 insertions(+), 1 deletion(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index d0be6d84..a66f023d 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -83,7 +83,7 @@ AGENT_SEED_DIR = '/var/lib/waagent' - IMDS_TIMEOUT_IN_SECONDS = 2 - IMDS_URL = "http://169.254.169.254/metadata" - IMDS_VER_MIN = "2019-06-01" --IMDS_VER_WANT = "2020-10-01" -+IMDS_VER_WANT = "2021-01-01" - - - # This holds SSH key data including if the source was -@@ -539,6 +539,20 @@ class DataSourceAzure(sources.DataSource): - imds_disable_password - ) - crawled_data['metadata']['disable_password'] = imds_disable_password # noqa: E501 -+ -+ # only use userdata from imds if OVF did not provide custom data -+ # userdata provided by IMDS is always base64 encoded -+ if not userdata_raw: -+ imds_userdata = _userdata_from_imds(imds_md) -+ if imds_userdata: -+ LOG.debug("Retrieved userdata from IMDS") -+ try: -+ crawled_data['userdata_raw'] = base64.b64decode( -+ ''.join(imds_userdata.split())) -+ except Exception: -+ report_diagnostic_event( -+ "Bad userdata in IMDS", -+ logger_func=LOG.warning) - found = cdev - - report_diagnostic_event( -@@ -1512,6 +1526,13 @@ def _username_from_imds(imds_data): - return None - - -+def _userdata_from_imds(imds_data): -+ try: -+ return imds_data['compute']['userData'] -+ except KeyError: -+ return None -+ -+ - def _hostname_from_imds(imds_data): - try: - return imds_data['compute']['osProfile']['computerName'] -diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py -index c4a8e08d..f8433690 100644 ---- a/tests/unittests/test_datasource/test_azure.py -+++ b/tests/unittests/test_datasource/test_azure.py -@@ -1899,6 +1899,56 @@ scbus-1 on xpt0 bus 0 - dsrc.get_data() - self.assertTrue(dsrc.metadata["disable_password"]) - -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_userdata_from_imds(self, m_get_metadata_from_imds): -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ odata = {'HostName': "myhost", 'UserName': "myuser"} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ userdata = "userdataImds" -+ imds_data = copy.deepcopy(NETWORK_METADATA) -+ imds_data["compute"]["osProfile"] = dict( -+ adminUsername="username1", -+ computerName="hostname1", -+ disablePasswordAuthentication="true", -+ ) -+ imds_data["compute"]["userData"] = b64e(userdata) -+ m_get_metadata_from_imds.return_value = imds_data -+ dsrc = self._get_ds(data) -+ ret = dsrc.get_data() -+ self.assertTrue(ret) -+ self.assertEqual(dsrc.userdata_raw, userdata.encode('utf-8')) -+ -+ @mock.patch(MOCKPATH + 'get_metadata_from_imds') -+ def test_userdata_from_imds_with_customdata_from_OVF( -+ self, m_get_metadata_from_imds): -+ userdataOVF = "userdataOVF" -+ odata = { -+ 'HostName': "myhost", 'UserName': "myuser", -+ 'UserData': {'text': b64e(userdataOVF), 'encoding': 'base64'} -+ } -+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} -+ data = { -+ 'ovfcontent': construct_valid_ovf_env(data=odata), -+ 'sys_cfg': sys_cfg -+ } -+ -+ userdataImds = "userdataImds" -+ imds_data = copy.deepcopy(NETWORK_METADATA) -+ imds_data["compute"]["osProfile"] = dict( -+ adminUsername="username1", -+ computerName="hostname1", -+ disablePasswordAuthentication="true", -+ ) -+ imds_data["compute"]["userData"] = b64e(userdataImds) -+ m_get_metadata_from_imds.return_value = imds_data -+ dsrc = self._get_ds(data) -+ ret = dsrc.get_data() -+ self.assertTrue(ret) -+ self.assertEqual(dsrc.userdata_raw, userdataOVF.encode('utf-8')) -+ - - class TestAzureBounce(CiTestCase): - --- -2.27.0 - diff --git a/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch b/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch deleted file mode 100644 index 6f6c109..0000000 --- a/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch +++ /dev/null @@ -1,177 +0,0 @@ -From 01489fb91f64f6137ddf88c39feabe4296f3a156 Mon Sep 17 00:00:00 2001 -From: Anh Vo <anhvo@microsoft.com> -Date: Fri, 23 Apr 2021 10:18:05 -0400 -Subject: [PATCH 4/7] Azure: eject the provisioning iso before reporting ready - (#861) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [4/7] ba830546a62ac5bea33b91d133d364a897b9f6c0 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -Due to hyper-v implementations, iso ejection is more efficient if performed -from within the guest. The code will attempt to perform a best-effort ejection. -Failure during ejection will not prevent reporting ready from happening. If iso -ejection is successful, later iso ejection from the platform will be a no-op. -In the event the iso ejection from the guest fails, iso ejection will still happen at -the platform level. ---- - cloudinit/sources/DataSourceAzure.py | 22 +++++++++++++++--- - cloudinit/sources/helpers/azure.py | 23 ++++++++++++++++--- - .../test_datasource/test_azure_helper.py | 13 +++++++++-- - 3 files changed, 50 insertions(+), 8 deletions(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index 020b7006..39e67c4f 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -332,6 +332,7 @@ class DataSourceAzure(sources.DataSource): - dsname = 'Azure' - _negotiated = False - _metadata_imds = sources.UNSET -+ _ci_pkl_version = 1 - - def __init__(self, sys_cfg, distro, paths): - sources.DataSource.__init__(self, sys_cfg, distro, paths) -@@ -346,8 +347,13 @@ class DataSourceAzure(sources.DataSource): - # Regenerate network config new_instance boot and every boot - self.update_events['network'].add(EventType.BOOT) - self._ephemeral_dhcp_ctx = None -- - self.failed_desired_api_version = False -+ self.iso_dev = None -+ -+ def _unpickle(self, ci_pkl_version: int) -> None: -+ super()._unpickle(ci_pkl_version) -+ if "iso_dev" not in self.__dict__: -+ self.iso_dev = None - - def __str__(self): - root = sources.DataSource.__str__(self) -@@ -459,6 +465,13 @@ class DataSourceAzure(sources.DataSource): - '%s was not mountable' % cdev, logger_func=LOG.warning) - continue - -+ report_diagnostic_event("Found provisioning metadata in %s" % cdev, -+ logger_func=LOG.debug) -+ -+ # save the iso device for ejection before reporting ready -+ if cdev.startswith("/dev"): -+ self.iso_dev = cdev -+ - perform_reprovision = reprovision or self._should_reprovision(ret) - perform_reprovision_after_nic_attach = ( - reprovision_after_nic_attach or -@@ -1226,7 +1239,9 @@ class DataSourceAzure(sources.DataSource): - @return: The success status of sending the ready signal. - """ - try: -- get_metadata_from_fabric(None, lease['unknown-245']) -+ get_metadata_from_fabric(fallback_lease_file=None, -+ dhcp_opts=lease['unknown-245'], -+ iso_dev=self.iso_dev) - return True - except Exception as e: - report_diagnostic_event( -@@ -1332,7 +1347,8 @@ class DataSourceAzure(sources.DataSource): - metadata_func = partial(get_metadata_from_fabric, - fallback_lease_file=self. - dhclient_lease_file, -- pubkey_info=pubkey_info) -+ pubkey_info=pubkey_info, -+ iso_dev=self.iso_dev) - - LOG.debug("negotiating with fabric via agent command %s", - self.ds_cfg['agent_command']) -diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py -index 03e7156b..ad476076 100755 ---- a/cloudinit/sources/helpers/azure.py -+++ b/cloudinit/sources/helpers/azure.py -@@ -865,7 +865,19 @@ class WALinuxAgentShim: - return endpoint_ip_address - - @azure_ds_telemetry_reporter -- def register_with_azure_and_fetch_data(self, pubkey_info=None) -> dict: -+ def eject_iso(self, iso_dev) -> None: -+ try: -+ LOG.debug("Ejecting the provisioning iso") -+ subp.subp(['eject', iso_dev]) -+ except Exception as e: -+ report_diagnostic_event( -+ "Failed ejecting the provisioning iso: %s" % e, -+ logger_func=LOG.debug) -+ -+ @azure_ds_telemetry_reporter -+ def register_with_azure_and_fetch_data(self, -+ pubkey_info=None, -+ iso_dev=None) -> dict: - """Gets the VM's GoalState from Azure, uses the GoalState information - to report ready/send the ready signal/provisioning complete signal to - Azure, and then uses pubkey_info to filter and obtain the user's -@@ -891,6 +903,10 @@ class WALinuxAgentShim: - ssh_keys = self._get_user_pubkeys(goal_state, pubkey_info) - health_reporter = GoalStateHealthReporter( - goal_state, self.azure_endpoint_client, self.endpoint) -+ -+ if iso_dev is not None: -+ self.eject_iso(iso_dev) -+ - health_reporter.send_ready_signal() - return {'public-keys': ssh_keys} - -@@ -1046,11 +1062,12 @@ class WALinuxAgentShim: - - @azure_ds_telemetry_reporter - def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None, -- pubkey_info=None): -+ pubkey_info=None, iso_dev=None): - shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file, - dhcp_options=dhcp_opts) - try: -- return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info) -+ return shim.register_with_azure_and_fetch_data( -+ pubkey_info=pubkey_info, iso_dev=iso_dev) - finally: - shim.clean_up() - -diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py -index 63482c6c..552c7905 100644 ---- a/tests/unittests/test_datasource/test_azure_helper.py -+++ b/tests/unittests/test_datasource/test_azure_helper.py -@@ -1009,6 +1009,14 @@ class TestWALinuxAgentShim(CiTestCase): - self.GoalState.return_value.container_id = self.test_container_id - self.GoalState.return_value.instance_id = self.test_instance_id - -+ def test_eject_iso_is_called(self): -+ shim = wa_shim() -+ with mock.patch.object( -+ shim, 'eject_iso', autospec=True -+ ) as m_eject_iso: -+ shim.register_with_azure_and_fetch_data(iso_dev="/dev/sr0") -+ m_eject_iso.assert_called_once_with("/dev/sr0") -+ - def test_http_client_does_not_use_certificate_for_report_ready(self): - shim = wa_shim() - shim.register_with_azure_and_fetch_data() -@@ -1283,13 +1291,14 @@ class TestGetMetadataGoalStateXMLAndReportReadyToFabric(CiTestCase): - - def test_calls_shim_register_with_azure_and_fetch_data(self): - m_pubkey_info = mock.MagicMock() -- azure_helper.get_metadata_from_fabric(pubkey_info=m_pubkey_info) -+ azure_helper.get_metadata_from_fabric( -+ pubkey_info=m_pubkey_info, iso_dev="/dev/sr0") - self.assertEqual( - 1, - self.m_shim.return_value - .register_with_azure_and_fetch_data.call_count) - self.assertEqual( -- mock.call(pubkey_info=m_pubkey_info), -+ mock.call(iso_dev="/dev/sr0", pubkey_info=m_pubkey_info), - self.m_shim.return_value - .register_with_azure_and_fetch_data.call_args) - --- -2.27.0 - diff --git a/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch b/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch deleted file mode 100644 index 627fd2b..0000000 --- a/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch +++ /dev/null @@ -1,90 +0,0 @@ -From f11bbe7f04a48eebcb446e283820d7592f76cf86 Mon Sep 17 00:00:00 2001 -From: Johnson Shi <Johnson.Shi@microsoft.com> -Date: Thu, 25 Mar 2021 07:20:10 -0700 -Subject: [PATCH 2/7] Azure helper: Ensure Azure http handler sleeps between - retries (#842) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [2/7] e8f8bb658b629a8444bd2ba19f109952acf33311 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -Ensure that the Azure helper's http handler sleeps a fixed duration -between retry failure attempts. The http handler will sleep a fixed -duration between failed attempts regardless of whether the attempt -failed due to (1) request timing out or (2) instant failure (no -timeout). - -Due to certain platform issues, the http request to the Azure endpoint -may instantly fail without reaching the http timeout duration. Without -sleeping a fixed duration in between retry attempts, the http handler -will loop through the max retry attempts quickly. This causes the -communication between cloud-init and the Azure platform to be less -resilient due to the short total duration if there is no sleep in -between retries. ---- - cloudinit/sources/helpers/azure.py | 2 ++ - tests/unittests/test_datasource/test_azure_helper.py | 11 +++++++++-- - 2 files changed, 11 insertions(+), 2 deletions(-) - -diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py -index d3055d08..03e7156b 100755 ---- a/cloudinit/sources/helpers/azure.py -+++ b/cloudinit/sources/helpers/azure.py -@@ -303,6 +303,7 @@ def http_with_retries(url, **kwargs) -> str: - - max_readurl_attempts = 240 - default_readurl_timeout = 5 -+ sleep_duration_between_retries = 5 - periodic_logging_attempts = 12 - - if 'timeout' not in kwargs: -@@ -338,6 +339,7 @@ def http_with_retries(url, **kwargs) -> str: - 'attempt %d with exception: %s' % - (url, attempt, e), - logger_func=LOG.debug) -+ time.sleep(sleep_duration_between_retries) - - raise exc - -diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py -index b8899807..63482c6c 100644 ---- a/tests/unittests/test_datasource/test_azure_helper.py -+++ b/tests/unittests/test_datasource/test_azure_helper.py -@@ -384,6 +384,7 @@ class TestAzureHelperHttpWithRetries(CiTestCase): - - max_readurl_attempts = 240 - default_readurl_timeout = 5 -+ sleep_duration_between_retries = 5 - periodic_logging_attempts = 12 - - def setUp(self): -@@ -394,8 +395,8 @@ class TestAzureHelperHttpWithRetries(CiTestCase): - self.m_readurl = patches.enter_context( - mock.patch.object( - azure_helper.url_helper, 'readurl', mock.MagicMock())) -- patches.enter_context( -- mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock())) -+ self.m_sleep = patches.enter_context( -+ mock.patch.object(azure_helper.time, 'sleep', autospec=True)) - - def test_http_with_retries(self): - self.m_readurl.return_value = 'TestResp' -@@ -438,6 +439,12 @@ class TestAzureHelperHttpWithRetries(CiTestCase): - self.m_readurl.call_count, - self.periodic_logging_attempts + 1) - -+ # Ensure that cloud-init did sleep between each failed request -+ self.assertEqual( -+ self.m_sleep.call_count, -+ self.periodic_logging_attempts) -+ self.m_sleep.assert_called_with(self.sleep_duration_between_retries) -+ - def test_http_with_retries_long_delay_logs_periodic_failure_msg(self): - self.m_readurl.side_effect = \ - [SentinelException] * self.periodic_logging_attempts + \ --- -2.27.0 - diff --git a/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch b/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch deleted file mode 100644 index 32fe4ac..0000000 --- a/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch +++ /dev/null @@ -1,47 +0,0 @@ -From c3d41dc6b18df0d74f569b1a0ba43c8118437948 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Fri, 14 Jan 2022 16:40:24 +0100 -Subject: [PATCH 3/6] Change netifaces dependency to 0.10.4 (#965) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 44: Datasource for VMware -RH-Commit: [3/6] d25d68427ab8b86ee1521c66483e9300e8fcc735 -RH-Bugzilla: 2026587 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -commit b9d308b4d61d22bacc05bcae59819755975631f8 -Author: Andrew Kutz <101085+akutz@users.noreply.github.com> -Date: Tue Aug 10 15:10:44 2021 -0500 - - Change netifaces dependency to 0.10.4 (#965) - - Change netifaces dependency to 0.10.4 - - Currently versions Ubuntu <=20.10 use netifaces 0.10.4 By requiring - netifaces 0.10.9, the VMware datasource omitted itself from cloud-init - on Ubuntu <=20.10. - - This patch changes the netifaces dependency to 0.10.4. While it is true - there are patches to netifaces post 0.10.4 that are desirable, testing - against the most common network configuration was performed to verify - the VMware datasource will still function with netifaces 0.10.4. - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - requirements.txt | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/requirements.txt b/requirements.txt -index 41d01d62..c4adc455 100644 ---- a/requirements.txt -+++ b/requirements.txt -@@ -40,4 +40,4 @@ jsonschema - # and still participate in instance-data by gathering the network in detail at - # runtime and merge that information into the metadata and repersist that to - # disk. --netifaces>=0.10.9 -+netifaces>=0.10.4 --- -2.27.0 - diff --git a/SOURCES/ci-Datasource-for-VMware-953.patch b/SOURCES/ci-Datasource-for-VMware-953.patch deleted file mode 100644 index 137ee07..0000000 --- a/SOURCES/ci-Datasource-for-VMware-953.patch +++ /dev/null @@ -1,2198 +0,0 @@ -From 1917af220242840ec1b21f82f80532cf6548cc00 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Fri, 14 Jan 2022 16:34:49 +0100 -Subject: [PATCH 2/6] Datasource for VMware (#953) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 44: Datasource for VMware -RH-Commit: [2/6] bb6e58dfeaf8b64d2801ddb4cb73868cf31de3ef -RH-Bugzilla: 2026587 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -commit 8b4a9bc7b81e61943af873bad92e2133f8275b0b -Author: Andrew Kutz <101085+akutz@users.noreply.github.com> -Date: Mon Aug 9 21:24:07 2021 -0500 - - Datasource for VMware (#953) - - This patch finally introduces the Cloud-Init Datasource for VMware - GuestInfo as a part of cloud-init proper. This datasource has existed - since 2018, and rapidly became the de facto datasource for developers - working with Packer, Terraform, for projects like kube-image-builder, - and the de jure datasource for Photon OS. - - The major change to the datasource from its previous incarnation is - the name. Now named DatasourceVMware, this new version of the - datasource will allow multiple transport types in addition to - GuestInfo keys. - - This datasource includes several unique features developed to address - real-world situations: - - * Support for reading any key (metadata, userdata, vendordata) both - from the guestinfo table when running on a VM in vSphere as well as - from an environment variable when running inside of a container, - useful for rapid dev/test. - - * Allows booting with DHCP while still providing full participation - in Cloud-Init instance data and Jinja queries. The netifaces library - provides the ability to inspect the network after it is online, - and the runtime network configuration is then merged into the - existing metadata and persisted to disk. - - * Advertises the local_ipv4 and local_ipv6 addresses via guestinfo - as well. This is useful as Guest Tools is not always able to - identify what would be considered the local address. - - The primary author and current steward of this datasource spoke at - Cloud-Init Con 2020 where there was interest in contributing this datasource - to the Cloud-Init codebase. - - The datasource currently lives in its own GitHub repository at - https://github.com/vmware/cloud-init-vmware-guestinfo. Once the datasource - is merged into Cloud-Init, the old repository will be deprecated. - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - README.md | 2 +- - cloudinit/settings.py | 1 + - cloudinit/sources/DataSourceVMware.py | 871 ++++++++++++++++++ - doc/rtd/topics/availability.rst | 1 + - doc/rtd/topics/datasources.rst | 2 +- - doc/rtd/topics/datasources/vmware.rst | 359 ++++++++ - requirements.txt | 9 + - .../unittests/test_datasource/test_common.py | 3 + - .../unittests/test_datasource/test_vmware.py | 377 ++++++++ - tests/unittests/test_ds_identify.py | 279 +++++- - tools/.github-cla-signers | 1 + - tools/ds-identify | 76 +- - 12 files changed, 1977 insertions(+), 4 deletions(-) - create mode 100644 cloudinit/sources/DataSourceVMware.py - create mode 100644 doc/rtd/topics/datasources/vmware.rst - create mode 100644 tests/unittests/test_datasource/test_vmware.py - -diff --git a/README.md b/README.md -index 435405da..aa4fad63 100644 ---- a/README.md -+++ b/README.md -@@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! - - | Supported OSes | Supported Public Clouds | Supported Private Clouds | - | --- | --- | --- | --| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />| -+| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br />VMware<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />| - - ## To start developing cloud-init - -diff --git a/cloudinit/settings.py b/cloudinit/settings.py -index 2acf2615..d5f32dbb 100644 ---- a/cloudinit/settings.py -+++ b/cloudinit/settings.py -@@ -42,6 +42,7 @@ CFG_BUILTIN = { - 'Exoscale', - 'RbxCloud', - 'UpCloud', -+ 'VMware', - # At the end to act as a 'catch' when none of the above work... - 'None', - ], -diff --git a/cloudinit/sources/DataSourceVMware.py b/cloudinit/sources/DataSourceVMware.py -new file mode 100644 -index 00000000..22ca63de ---- /dev/null -+++ b/cloudinit/sources/DataSourceVMware.py -@@ -0,0 +1,871 @@ -+# Cloud-Init DataSource for VMware -+# -+# Copyright (c) 2018-2021 VMware, Inc. All Rights Reserved. -+# -+# Authors: Anish Swaminathan <anishs@vmware.com> -+# Andrew Kutz <akutz@vmware.com> -+# -+# This file is part of cloud-init. See LICENSE file for license information. -+ -+"""Cloud-Init DataSource for VMware -+ -+This module provides a cloud-init datasource for VMware systems and supports -+multiple transports types, including: -+ -+ * EnvVars -+ * GuestInfo -+ -+Netifaces (https://github.com/al45tair/netifaces) -+ -+ Please note this module relies on the netifaces project to introspect the -+ runtime, network configuration of the host on which this datasource is -+ running. This is in contrast to the rest of cloud-init which uses the -+ cloudinit/netinfo module. -+ -+ The reasons for using netifaces include: -+ -+ * Netifaces is built in C and is more portable across multiple systems -+ and more deterministic than shell exec'ing local network commands and -+ parsing their output. -+ -+ * Netifaces provides a stable way to determine the view of the host's -+ network after DHCP has brought the network online. Unlike most other -+ datasources, this datasource still provides support for JINJA queries -+ based on networking information even when the network is based on a -+ DHCP lease. While this does not tie this datasource directly to -+ netifaces, it does mean the ability to consistently obtain the -+ correct information is paramount. -+ -+ * It is currently possible to execute this datasource on macOS -+ (which many developers use today) to print the output of the -+ get_host_info function. This function calls netifaces to obtain -+ the same runtime network configuration that the datasource would -+ persist to the local system's instance data. -+ -+ However, the netinfo module fails on macOS. The result is either a -+ hung operation that requires a SIGINT to return control to the user, -+ or, if brew is used to install iproute2mac, the ip commands are used -+ but produce output the netinfo module is unable to parse. -+ -+ While macOS is not a target of cloud-init, this feature is quite -+ useful when working on this datasource. -+ -+ For more information about this behavior, please see the following -+ PR comment, https://bit.ly/3fG7OVh. -+ -+ The authors of this datasource are not opposed to moving away from -+ netifaces. The goal may be to eventually do just that. This proviso was -+ added to the top of this module as a way to remind future-us and others -+ why netifaces was used in the first place in order to either smooth the -+ transition away from netifaces or embrace it further up the cloud-init -+ stack. -+""" -+ -+import collections -+import copy -+from distutils.spawn import find_executable -+import ipaddress -+import json -+import os -+import socket -+import time -+ -+from cloudinit import dmi, log as logging -+from cloudinit import sources -+from cloudinit import util -+from cloudinit.subp import subp, ProcessExecutionError -+ -+import netifaces -+ -+ -+PRODUCT_UUID_FILE_PATH = "/sys/class/dmi/id/product_uuid" -+ -+LOG = logging.getLogger(__name__) -+NOVAL = "No value found" -+ -+DATA_ACCESS_METHOD_ENVVAR = "envvar" -+DATA_ACCESS_METHOD_GUESTINFO = "guestinfo" -+ -+VMWARE_RPCTOOL = find_executable("vmware-rpctool") -+REDACT = "redact" -+CLEANUP_GUESTINFO = "cleanup-guestinfo" -+VMX_GUESTINFO = "VMX_GUESTINFO" -+GUESTINFO_EMPTY_YAML_VAL = "---" -+ -+LOCAL_IPV4 = "local-ipv4" -+LOCAL_IPV6 = "local-ipv6" -+WAIT_ON_NETWORK = "wait-on-network" -+WAIT_ON_NETWORK_IPV4 = "ipv4" -+WAIT_ON_NETWORK_IPV6 = "ipv6" -+ -+ -+class DataSourceVMware(sources.DataSource): -+ """ -+ Setting the hostname: -+ The hostname is set by way of the metadata key "local-hostname". -+ -+ Setting the instance ID: -+ The instance ID may be set by way of the metadata key "instance-id". -+ However, if this value is absent then the instance ID is read -+ from the file /sys/class/dmi/id/product_uuid. -+ -+ Configuring the network: -+ The network is configured by setting the metadata key "network" -+ with a value consistent with Network Config Versions 1 or 2, -+ depending on the Linux distro's version of cloud-init: -+ -+ Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1 -+ Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2 -+ -+ For example, CentOS 7's official cloud-init package is version -+ 0.7.9 and does not support Network Config Version 2. However, -+ this datasource still supports supplying Network Config Version 2 -+ data as long as the Linux distro's cloud-init package is new -+ enough to parse the data. -+ -+ The metadata key "network.encoding" may be used to indicate the -+ format of the metadata key "network". Valid encodings are base64 -+ and gzip+base64. -+ """ -+ -+ dsname = "VMware" -+ -+ def __init__(self, sys_cfg, distro, paths, ud_proc=None): -+ sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc) -+ -+ self.data_access_method = None -+ self.vmware_rpctool = VMWARE_RPCTOOL -+ -+ def _get_data(self): -+ """ -+ _get_data loads the metadata, userdata, and vendordata from one of -+ the following locations in the given order: -+ -+ * envvars -+ * guestinfo -+ -+ Please note when updating this function with support for new data -+ transports, the order should match the order in the dscheck_VMware -+ function from the file ds-identify. -+ """ -+ -+ # Initialize the locally scoped metadata, userdata, and vendordata -+ # variables. They are assigned below depending on the detected data -+ # access method. -+ md, ud, vd = None, None, None -+ -+ # First check to see if there is data via env vars. -+ if os.environ.get(VMX_GUESTINFO, ""): -+ md = guestinfo_envvar("metadata") -+ ud = guestinfo_envvar("userdata") -+ vd = guestinfo_envvar("vendordata") -+ -+ if md or ud or vd: -+ self.data_access_method = DATA_ACCESS_METHOD_ENVVAR -+ -+ # At this point, all additional data transports are valid only on -+ # a VMware platform. -+ if not self.data_access_method: -+ system_type = dmi.read_dmi_data("system-product-name") -+ if system_type is None: -+ LOG.debug("No system-product-name found") -+ return False -+ if "vmware" not in system_type.lower(): -+ LOG.debug("Not a VMware platform") -+ return False -+ -+ # If no data was detected, check the guestinfo transport next. -+ if not self.data_access_method: -+ if self.vmware_rpctool: -+ md = guestinfo("metadata", self.vmware_rpctool) -+ ud = guestinfo("userdata", self.vmware_rpctool) -+ vd = guestinfo("vendordata", self.vmware_rpctool) -+ -+ if md or ud or vd: -+ self.data_access_method = DATA_ACCESS_METHOD_GUESTINFO -+ -+ if not self.data_access_method: -+ LOG.error("failed to find a valid data access method") -+ return False -+ -+ LOG.info("using data access method %s", self._get_subplatform()) -+ -+ # Get the metadata. -+ self.metadata = process_metadata(load_json_or_yaml(md)) -+ -+ # Get the user data. -+ self.userdata_raw = ud -+ -+ # Get the vendor data. -+ self.vendordata_raw = vd -+ -+ # Redact any sensitive information. -+ self.redact_keys() -+ -+ # get_data returns true if there is any available metadata, -+ # userdata, or vendordata. -+ if self.metadata or self.userdata_raw or self.vendordata_raw: -+ return True -+ else: -+ return False -+ -+ def setup(self, is_new_instance): -+ """setup(is_new_instance) -+ -+ This is called before user-data and vendor-data have been processed. -+ -+ Unless the datasource has set mode to 'local', then networking -+ per 'fallback' or per 'network_config' will have been written and -+ brought up the OS at this point. -+ """ -+ -+ host_info = wait_on_network(self.metadata) -+ LOG.info("got host-info: %s", host_info) -+ -+ # Reflect any possible local IPv4 or IPv6 addresses in the guest -+ # info. -+ advertise_local_ip_addrs(host_info) -+ -+ # Ensure the metadata gets updated with information about the -+ # host, including the network interfaces, default IP addresses, -+ # etc. -+ self.metadata = util.mergemanydict([self.metadata, host_info]) -+ -+ # Persist the instance data for versions of cloud-init that support -+ # doing so. This occurs here rather than in the get_data call in -+ # order to ensure that the network interfaces are up and can be -+ # persisted with the metadata. -+ self.persist_instance_data() -+ -+ def _get_subplatform(self): -+ get_key_name_fn = None -+ if self.data_access_method == DATA_ACCESS_METHOD_ENVVAR: -+ get_key_name_fn = get_guestinfo_envvar_key_name -+ elif self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO: -+ get_key_name_fn = get_guestinfo_key_name -+ else: -+ return sources.METADATA_UNKNOWN -+ -+ return "%s (%s)" % ( -+ self.data_access_method, -+ get_key_name_fn("metadata"), -+ ) -+ -+ @property -+ def network_config(self): -+ if "network" in self.metadata: -+ LOG.debug("using metadata network config") -+ else: -+ LOG.debug("using fallback network config") -+ self.metadata["network"] = { -+ "config": self.distro.generate_fallback_config(), -+ } -+ return self.metadata["network"]["config"] -+ -+ def get_instance_id(self): -+ # Pull the instance ID out of the metadata if present. Otherwise -+ # read the file /sys/class/dmi/id/product_uuid for the instance ID. -+ if self.metadata and "instance-id" in self.metadata: -+ return self.metadata["instance-id"] -+ with open(PRODUCT_UUID_FILE_PATH, "r") as id_file: -+ self.metadata["instance-id"] = str(id_file.read()).rstrip().lower() -+ return self.metadata["instance-id"] -+ -+ def get_public_ssh_keys(self): -+ for key_name in ( -+ "public-keys-data", -+ "public_keys_data", -+ "public-keys", -+ "public_keys", -+ ): -+ if key_name in self.metadata: -+ return sources.normalize_pubkey_data(self.metadata[key_name]) -+ return [] -+ -+ def redact_keys(self): -+ # Determine if there are any keys to redact. -+ keys_to_redact = None -+ if REDACT in self.metadata: -+ keys_to_redact = self.metadata[REDACT] -+ elif CLEANUP_GUESTINFO in self.metadata: -+ # This is for backwards compatibility. -+ keys_to_redact = self.metadata[CLEANUP_GUESTINFO] -+ -+ if self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO: -+ guestinfo_redact_keys(keys_to_redact, self.vmware_rpctool) -+ -+ -+def decode(key, enc_type, data): -+ """ -+ decode returns the decoded string value of data -+ key is a string used to identify the data being decoded in log messages -+ """ -+ LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type) -+ -+ raw_data = None -+ if enc_type in ["gzip+base64", "gz+b64"]: -+ LOG.debug("Decoding %s format %s", enc_type, key) -+ raw_data = util.decomp_gzip(util.b64d(data)) -+ elif enc_type in ["base64", "b64"]: -+ LOG.debug("Decoding %s format %s", enc_type, key) -+ raw_data = util.b64d(data) -+ else: -+ LOG.debug("Plain-text data %s", key) -+ raw_data = data -+ -+ return util.decode_binary(raw_data) -+ -+ -+def get_none_if_empty_val(val): -+ """ -+ get_none_if_empty_val returns None if the provided value, once stripped -+ of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL. -+ -+ The return value is always a string, regardless of whether the input is -+ a bytes class or a string. -+ """ -+ -+ # If the provided value is a bytes class, convert it to a string to -+ # simplify the rest of this function's logic. -+ val = util.decode_binary(val) -+ val = val.rstrip() -+ if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL: -+ return None -+ return val -+ -+ -+def advertise_local_ip_addrs(host_info): -+ """ -+ advertise_local_ip_addrs gets the local IP address information from -+ the provided host_info map and sets the addresses in the guestinfo -+ namespace -+ """ -+ if not host_info: -+ return -+ -+ # Reflect any possible local IPv4 or IPv6 addresses in the guest -+ # info. -+ local_ipv4 = host_info.get(LOCAL_IPV4) -+ if local_ipv4: -+ guestinfo_set_value(LOCAL_IPV4, local_ipv4) -+ LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4) -+ -+ local_ipv6 = host_info.get(LOCAL_IPV6) -+ if local_ipv6: -+ guestinfo_set_value(LOCAL_IPV6, local_ipv6) -+ LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6) -+ -+ -+def handle_returned_guestinfo_val(key, val): -+ """ -+ handle_returned_guestinfo_val returns the provided value if it is -+ not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is -+ returned -+ """ -+ val = get_none_if_empty_val(val) -+ if val: -+ return val -+ LOG.debug("No value found for key %s", key) -+ return None -+ -+ -+def get_guestinfo_key_name(key): -+ return "guestinfo." + key -+ -+ -+def get_guestinfo_envvar_key_name(key): -+ return ("vmx." + get_guestinfo_key_name(key)).upper().replace(".", "_", -1) -+ -+ -+def guestinfo_envvar(key): -+ val = guestinfo_envvar_get_value(key) -+ if not val: -+ return None -+ enc_type = guestinfo_envvar_get_value(key + ".encoding") -+ return decode(get_guestinfo_envvar_key_name(key), enc_type, val) -+ -+ -+def guestinfo_envvar_get_value(key): -+ env_key = get_guestinfo_envvar_key_name(key) -+ return handle_returned_guestinfo_val(key, os.environ.get(env_key, "")) -+ -+ -+def guestinfo(key, vmware_rpctool=VMWARE_RPCTOOL): -+ """ -+ guestinfo returns the guestinfo value for the provided key, decoding -+ the value when required -+ """ -+ val = guestinfo_get_value(key, vmware_rpctool) -+ if not val: -+ return None -+ enc_type = guestinfo_get_value(key + ".encoding", vmware_rpctool) -+ return decode(get_guestinfo_key_name(key), enc_type, val) -+ -+ -+def guestinfo_get_value(key, vmware_rpctool=VMWARE_RPCTOOL): -+ """ -+ Returns a guestinfo value for the specified key. -+ """ -+ LOG.debug("Getting guestinfo value for key %s", key) -+ -+ try: -+ (stdout, stderr) = subp( -+ [ -+ vmware_rpctool, -+ "info-get " + get_guestinfo_key_name(key), -+ ] -+ ) -+ if stderr == NOVAL: -+ LOG.debug("No value found for key %s", key) -+ elif not stdout: -+ LOG.error("Failed to get guestinfo value for key %s", key) -+ return handle_returned_guestinfo_val(key, stdout) -+ except ProcessExecutionError as error: -+ if error.stderr == NOVAL: -+ LOG.debug("No value found for key %s", key) -+ else: -+ util.logexc( -+ LOG, -+ "Failed to get guestinfo value for key %s: %s", -+ key, -+ error, -+ ) -+ except Exception: -+ util.logexc( -+ LOG, -+ "Unexpected error while trying to get " -+ + "guestinfo value for key %s", -+ key, -+ ) -+ -+ return None -+ -+ -+def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL): -+ """ -+ Sets a guestinfo value for the specified key. Set value to an empty string -+ to clear an existing guestinfo key. -+ """ -+ -+ # If value is an empty string then set it to a single space as it is not -+ # possible to set a guestinfo key to an empty string. Setting a guestinfo -+ # key to a single space is as close as it gets to clearing an existing -+ # guestinfo key. -+ if value == "": -+ value = " " -+ -+ LOG.debug("Setting guestinfo key=%s to value=%s", key, value) -+ -+ try: -+ subp( -+ [ -+ vmware_rpctool, -+ ("info-set %s %s" % (get_guestinfo_key_name(key), value)), -+ ] -+ ) -+ return True -+ except ProcessExecutionError as error: -+ util.logexc( -+ LOG, -+ "Failed to set guestinfo key=%s to value=%s: %s", -+ key, -+ value, -+ error, -+ ) -+ except Exception: -+ util.logexc( -+ LOG, -+ "Unexpected error while trying to set " -+ + "guestinfo key=%s to value=%s", -+ key, -+ value, -+ ) -+ -+ return None -+ -+ -+def guestinfo_redact_keys(keys, vmware_rpctool=VMWARE_RPCTOOL): -+ """ -+ guestinfo_redact_keys redacts guestinfo of all of the keys in the given -+ list. each key will have its value set to "---". Since the value is valid -+ YAML, cloud-init can still read it if it tries. -+ """ -+ if not keys: -+ return -+ if not type(keys) in (list, tuple): -+ keys = [keys] -+ for key in keys: -+ key_name = get_guestinfo_key_name(key) -+ LOG.info("clearing %s", key_name) -+ if not guestinfo_set_value( -+ key, GUESTINFO_EMPTY_YAML_VAL, vmware_rpctool -+ ): -+ LOG.error("failed to clear %s", key_name) -+ LOG.info("clearing %s.encoding", key_name) -+ if not guestinfo_set_value(key + ".encoding", "", vmware_rpctool): -+ LOG.error("failed to clear %s.encoding", key_name) -+ -+ -+def load_json_or_yaml(data): -+ """ -+ load first attempts to unmarshal the provided data as JSON, and if -+ that fails then attempts to unmarshal the data as YAML. If data is -+ None then a new dictionary is returned. -+ """ -+ if not data: -+ return {} -+ try: -+ return util.load_json(data) -+ except (json.JSONDecodeError, TypeError): -+ return util.load_yaml(data) -+ -+ -+def process_metadata(data): -+ """ -+ process_metadata processes metadata and loads the optional network -+ configuration. -+ """ -+ network = None -+ if "network" in data: -+ network = data["network"] -+ del data["network"] -+ -+ network_enc = None -+ if "network.encoding" in data: -+ network_enc = data["network.encoding"] -+ del data["network.encoding"] -+ -+ if network: -+ if isinstance(network, collections.abc.Mapping): -+ LOG.debug("network data copied to 'config' key") -+ network = {"config": copy.deepcopy(network)} -+ else: -+ LOG.debug("network data to be decoded %s", network) -+ dec_net = decode("metadata.network", network_enc, network) -+ network = { -+ "config": load_json_or_yaml(dec_net), -+ } -+ -+ LOG.debug("network data %s", network) -+ data["network"] = network -+ -+ return data -+ -+ -+# Used to match classes to dependencies -+datasources = [ -+ (DataSourceVMware, (sources.DEP_FILESYSTEM,)), # Run at init-local -+ (DataSourceVMware, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), -+] -+ -+ -+def get_datasource_list(depends): -+ """ -+ Return a list of data sources that match this set of dependencies -+ """ -+ return sources.list_from_depends(depends, datasources) -+ -+ -+def get_default_ip_addrs(): -+ """ -+ Returns the default IPv4 and IPv6 addresses based on the device(s) used for -+ the default route. Please note that None may be returned for either address -+ family if that family has no default route or if there are multiple -+ addresses associated with the device used by the default route for a given -+ address. -+ """ -+ # TODO(promote and use netifaces in cloudinit.net* modules) -+ gateways = netifaces.gateways() -+ if "default" not in gateways: -+ return None, None -+ -+ default_gw = gateways["default"] -+ if ( -+ netifaces.AF_INET not in default_gw -+ and netifaces.AF_INET6 not in default_gw -+ ): -+ return None, None -+ -+ ipv4 = None -+ ipv6 = None -+ -+ gw4 = default_gw.get(netifaces.AF_INET) -+ if gw4: -+ _, dev4 = gw4 -+ addr4_fams = netifaces.ifaddresses(dev4) -+ if addr4_fams: -+ af_inet4 = addr4_fams.get(netifaces.AF_INET) -+ if af_inet4: -+ if len(af_inet4) > 1: -+ LOG.warning( -+ "device %s has more than one ipv4 address: %s", -+ dev4, -+ af_inet4, -+ ) -+ elif "addr" in af_inet4[0]: -+ ipv4 = af_inet4[0]["addr"] -+ -+ # Try to get the default IPv6 address by first seeing if there is a default -+ # IPv6 route. -+ gw6 = default_gw.get(netifaces.AF_INET6) -+ if gw6: -+ _, dev6 = gw6 -+ addr6_fams = netifaces.ifaddresses(dev6) -+ if addr6_fams: -+ af_inet6 = addr6_fams.get(netifaces.AF_INET6) -+ if af_inet6: -+ if len(af_inet6) > 1: -+ LOG.warning( -+ "device %s has more than one ipv6 address: %s", -+ dev6, -+ af_inet6, -+ ) -+ elif "addr" in af_inet6[0]: -+ ipv6 = af_inet6[0]["addr"] -+ -+ # If there is a default IPv4 address but not IPv6, then see if there is a -+ # single IPv6 address associated with the same device associated with the -+ # default IPv4 address. -+ if ipv4 and not ipv6: -+ af_inet6 = addr4_fams.get(netifaces.AF_INET6) -+ if af_inet6: -+ if len(af_inet6) > 1: -+ LOG.warning( -+ "device %s has more than one ipv6 address: %s", -+ dev4, -+ af_inet6, -+ ) -+ elif "addr" in af_inet6[0]: -+ ipv6 = af_inet6[0]["addr"] -+ -+ # If there is a default IPv6 address but not IPv4, then see if there is a -+ # single IPv4 address associated with the same device associated with the -+ # default IPv6 address. -+ if not ipv4 and ipv6: -+ af_inet4 = addr6_fams.get(netifaces.AF_INET) -+ if af_inet4: -+ if len(af_inet4) > 1: -+ LOG.warning( -+ "device %s has more than one ipv4 address: %s", -+ dev6, -+ af_inet4, -+ ) -+ elif "addr" in af_inet4[0]: -+ ipv4 = af_inet4[0]["addr"] -+ -+ return ipv4, ipv6 -+ -+ -+# patched socket.getfqdn() - see https://bugs.python.org/issue5004 -+ -+ -+def getfqdn(name=""): -+ """Get fully qualified domain name from name. -+ An empty argument is interpreted as meaning the local host. -+ """ -+ # TODO(may want to promote this function to util.getfqdn) -+ # TODO(may want to extend util.get_hostname to accept fqdn=True param) -+ name = name.strip() -+ if not name or name == "0.0.0.0": -+ name = util.get_hostname() -+ try: -+ addrs = socket.getaddrinfo( -+ name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME -+ ) -+ except socket.error: -+ pass -+ else: -+ for addr in addrs: -+ if addr[3]: -+ name = addr[3] -+ break -+ return name -+ -+ -+def is_valid_ip_addr(val): -+ """ -+ Returns false if the address is loopback, link local or unspecified; -+ otherwise true is returned. -+ """ -+ # TODO(extend cloudinit.net.is_ip_addr exclude link_local/loopback etc) -+ # TODO(migrate to use cloudinit.net.is_ip_addr)# -+ -+ addr = None -+ try: -+ addr = ipaddress.ip_address(val) -+ except ipaddress.AddressValueError: -+ addr = ipaddress.ip_address(str(val)) -+ except Exception: -+ return None -+ -+ if addr.is_link_local or addr.is_loopback or addr.is_unspecified: -+ return False -+ return True -+ -+ -+def get_host_info(): -+ """ -+ Returns host information such as the host name and network interfaces. -+ """ -+ # TODO(look to promote netifices use up in cloud-init netinfo funcs) -+ host_info = { -+ "network": { -+ "interfaces": { -+ "by-mac": collections.OrderedDict(), -+ "by-ipv4": collections.OrderedDict(), -+ "by-ipv6": collections.OrderedDict(), -+ }, -+ }, -+ } -+ hostname = getfqdn(util.get_hostname()) -+ if hostname: -+ host_info["hostname"] = hostname -+ host_info["local-hostname"] = hostname -+ host_info["local_hostname"] = hostname -+ -+ default_ipv4, default_ipv6 = get_default_ip_addrs() -+ if default_ipv4: -+ host_info[LOCAL_IPV4] = default_ipv4 -+ if default_ipv6: -+ host_info[LOCAL_IPV6] = default_ipv6 -+ -+ by_mac = host_info["network"]["interfaces"]["by-mac"] -+ by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"] -+ by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"] -+ -+ ifaces = netifaces.interfaces() -+ for dev_name in ifaces: -+ addr_fams = netifaces.ifaddresses(dev_name) -+ af_link = addr_fams.get(netifaces.AF_LINK) -+ af_inet4 = addr_fams.get(netifaces.AF_INET) -+ af_inet6 = addr_fams.get(netifaces.AF_INET6) -+ -+ mac = None -+ if af_link and "addr" in af_link[0]: -+ mac = af_link[0]["addr"] -+ -+ # Do not bother recording localhost -+ if mac == "00:00:00:00:00:00": -+ continue -+ -+ if mac and (af_inet4 or af_inet6): -+ key = mac -+ val = {} -+ if af_inet4: -+ af_inet4_vals = [] -+ for ip_info in af_inet4: -+ if not is_valid_ip_addr(ip_info["addr"]): -+ continue -+ af_inet4_vals.append(ip_info) -+ val["ipv4"] = af_inet4_vals -+ if af_inet6: -+ af_inet6_vals = [] -+ for ip_info in af_inet6: -+ if not is_valid_ip_addr(ip_info["addr"]): -+ continue -+ af_inet6_vals.append(ip_info) -+ val["ipv6"] = af_inet6_vals -+ by_mac[key] = val -+ -+ if af_inet4: -+ for ip_info in af_inet4: -+ key = ip_info["addr"] -+ if not is_valid_ip_addr(key): -+ continue -+ val = copy.deepcopy(ip_info) -+ del val["addr"] -+ if mac: -+ val["mac"] = mac -+ by_ipv4[key] = val -+ -+ if af_inet6: -+ for ip_info in af_inet6: -+ key = ip_info["addr"] -+ if not is_valid_ip_addr(key): -+ continue -+ val = copy.deepcopy(ip_info) -+ del val["addr"] -+ if mac: -+ val["mac"] = mac -+ by_ipv6[key] = val -+ -+ return host_info -+ -+ -+def wait_on_network(metadata): -+ # Determine whether we need to wait on the network coming online. -+ wait_on_ipv4 = False -+ wait_on_ipv6 = False -+ if WAIT_ON_NETWORK in metadata: -+ wait_on_network = metadata[WAIT_ON_NETWORK] -+ if WAIT_ON_NETWORK_IPV4 in wait_on_network: -+ wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4] -+ if isinstance(wait_on_ipv4_val, bool): -+ wait_on_ipv4 = wait_on_ipv4_val -+ else: -+ wait_on_ipv4 = util.translate_bool(wait_on_ipv4_val) -+ if WAIT_ON_NETWORK_IPV6 in wait_on_network: -+ wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6] -+ if isinstance(wait_on_ipv6_val, bool): -+ wait_on_ipv6 = wait_on_ipv6_val -+ else: -+ wait_on_ipv6 = util.translate_bool(wait_on_ipv6_val) -+ -+ # Get information about the host. -+ host_info = None -+ while host_info is None: -+ # This loop + sleep results in two logs every second while waiting -+ # for either ipv4 or ipv6 up. Do we really need to log each iteration -+ # or can we log once and log on successful exit? -+ host_info = get_host_info() -+ -+ network = host_info.get("network") or {} -+ interfaces = network.get("interfaces") or {} -+ by_ipv4 = interfaces.get("by-ipv4") or {} -+ by_ipv6 = interfaces.get("by-ipv6") or {} -+ -+ if wait_on_ipv4: -+ ipv4_ready = len(by_ipv4) > 0 if by_ipv4 else False -+ if not ipv4_ready: -+ host_info = None -+ -+ if wait_on_ipv6: -+ ipv6_ready = len(by_ipv6) > 0 if by_ipv6 else False -+ if not ipv6_ready: -+ host_info = None -+ -+ if host_info is None: -+ LOG.debug( -+ "waiting on network: wait4=%s, ready4=%s, wait6=%s, ready6=%s", -+ wait_on_ipv4, -+ ipv4_ready, -+ wait_on_ipv6, -+ ipv6_ready, -+ ) -+ time.sleep(1) -+ -+ LOG.debug("waiting on network complete") -+ return host_info -+ -+ -+def main(): -+ """ -+ Executed when this file is used as a program. -+ """ -+ try: -+ logging.setupBasicLogging() -+ except Exception: -+ pass -+ metadata = { -+ "wait-on-network": {"ipv4": True, "ipv6": "false"}, -+ "network": {"config": {"dhcp": True}}, -+ } -+ host_info = wait_on_network(metadata) -+ metadata = util.mergemanydict([metadata, host_info]) -+ print(util.json_dumps(metadata)) -+ -+ -+if __name__ == "__main__": -+ main() -+ -+# vi: ts=4 expandtab -diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst -index f58b2b38..6606367c 100644 ---- a/doc/rtd/topics/availability.rst -+++ b/doc/rtd/topics/availability.rst -@@ -64,5 +64,6 @@ Additionally, cloud-init is supported on these private clouds: - - LXD - - KVM - - Metal-as-a-Service (MAAS) -+- VMware - - .. vi: textwidth=79 -diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst -index 228173d2..8afed470 100644 ---- a/doc/rtd/topics/datasources.rst -+++ b/doc/rtd/topics/datasources.rst -@@ -49,7 +49,7 @@ The following is a list of documents for each supported datasource: - datasources/smartos.rst - datasources/upcloud.rst - datasources/zstack.rst -- -+ datasources/vmware.rst - - Creation - ======== -diff --git a/doc/rtd/topics/datasources/vmware.rst b/doc/rtd/topics/datasources/vmware.rst -new file mode 100644 -index 00000000..996eb61f ---- /dev/null -+++ b/doc/rtd/topics/datasources/vmware.rst -@@ -0,0 +1,359 @@ -+.. _datasource_vmware: -+ -+VMware -+====== -+ -+This datasource is for use with systems running on a VMware platform such as -+vSphere and currently supports the following data transports: -+ -+ -+* `GuestInfo <https://github.com/vmware/govmomi/blob/master/govc/USAGE.md#vmchange>`_ keys -+ -+Configuration -+------------- -+ -+The configuration method is dependent upon the transport: -+ -+GuestInfo Keys -+^^^^^^^^^^^^^^ -+ -+One method of providing meta, user, and vendor data is by setting the following -+key/value pairs on a VM's ``extraConfig`` `property <https://vdc-repo.vmware.com/vmwb-repository/dcr-public/723e7f8b-4f21-448b-a830-5f22fd931b01/5a8257bd-7f41-4423-9a73-03307535bd42/doc/vim.vm.ConfigInfo.html>`_ : -+ -+.. list-table:: -+ :header-rows: 1 -+ -+ * - Property -+ - Description -+ * - ``guestinfo.metadata`` -+ - A YAML or JSON document containing the cloud-init metadata. -+ * - ``guestinfo.metadata.encoding`` -+ - The encoding type for ``guestinfo.metadata``. -+ * - ``guestinfo.userdata`` -+ - A YAML document containing the cloud-init user data. -+ * - ``guestinfo.userdata.encoding`` -+ - The encoding type for ``guestinfo.userdata``. -+ * - ``guestinfo.vendordata`` -+ - A YAML document containing the cloud-init vendor data. -+ * - ``guestinfo.vendordata.encoding`` -+ - The encoding type for ``guestinfo.vendordata``. -+ -+ -+All ``guestinfo.*.encoding`` values may be set to ``base64`` or -+``gzip+base64``. -+ -+Features -+-------- -+ -+This section reviews several features available in this datasource, regardless -+of how the meta, user, and vendor data was discovered. -+ -+Instance data and lazy networks -+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -+ -+One of the hallmarks of cloud-init is `its use of instance-data and JINJA -+queries <../instancedata.html#using-instance-data>`_ -+-- the ability to write queries in user and vendor data that reference runtime -+information present in ``/run/cloud-init/instance-data.json``. This works well -+when the metadata provides all of the information up front, such as the network -+configuration. For systems that rely on DHCP, however, this information may not -+be available when the metadata is persisted to disk. -+ -+This datasource ensures that even if the instance is using DHCP to configure -+networking, the same details about the configured network are available in -+``/run/cloud-init/instance-data.json`` as if static networking was used. This -+information collected at runtime is easy to demonstrate by executing the -+datasource on the command line. From the root of this repository, run the -+following command: -+ -+.. code-block:: bash -+ -+ PYTHONPATH="$(pwd)" python3 cloudinit/sources/DataSourceVMware.py -+ -+The above command will result in output similar to the below JSON: -+ -+.. code-block:: json -+ -+ { -+ "hostname": "akutz.localhost", -+ "local-hostname": "akutz.localhost", -+ "local-ipv4": "192.168.0.188", -+ "local_hostname": "akutz.localhost", -+ "network": { -+ "config": { -+ "dhcp": true -+ }, -+ "interfaces": { -+ "by-ipv4": { -+ "172.0.0.2": { -+ "netmask": "255.255.255.255", -+ "peer": "172.0.0.2" -+ }, -+ "192.168.0.188": { -+ "broadcast": "192.168.0.255", -+ "mac": "64:4b:f0:18:9a:21", -+ "netmask": "255.255.255.0" -+ } -+ }, -+ "by-ipv6": { -+ "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2": { -+ "flags": 208, -+ "mac": "64:4b:f0:18:9a:21", -+ "netmask": "ffff:ffff:ffff:ffff::/64" -+ } -+ }, -+ "by-mac": { -+ "64:4b:f0:18:9a:21": { -+ "ipv4": [ -+ { -+ "addr": "192.168.0.188", -+ "broadcast": "192.168.0.255", -+ "netmask": "255.255.255.0" -+ } -+ ], -+ "ipv6": [ -+ { -+ "addr": "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2", -+ "flags": 208, -+ "netmask": "ffff:ffff:ffff:ffff::/64" -+ } -+ ] -+ }, -+ "ac:de:48:00:11:22": { -+ "ipv6": [] -+ } -+ } -+ } -+ }, -+ "wait-on-network": { -+ "ipv4": true, -+ "ipv6": "false" -+ } -+ } -+ -+ -+Redacting sensitive information -+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -+ -+Sometimes the cloud-init userdata might contain sensitive information, and it -+may be desirable to have the ``guestinfo.userdata`` key (or other guestinfo -+keys) redacted as soon as its data is read by the datasource. This is possible -+by adding the following to the metadata: -+ -+.. code-block:: yaml -+ -+ redact: # formerly named cleanup-guestinfo, which will also work -+ - userdata -+ - vendordata -+ -+When the above snippet is added to the metadata, the datasource will iterate -+over the elements in the ``redact`` array and clear each of the keys. For -+example, when the guestinfo transport is used, the above snippet will cause -+the following commands to be executed: -+ -+.. code-block:: shell -+ -+ vmware-rpctool "info-set guestinfo.userdata ---" -+ vmware-rpctool "info-set guestinfo.userdata.encoding " -+ vmware-rpctool "info-set guestinfo.vendordata ---" -+ vmware-rpctool "info-set guestinfo.vendordata.encoding " -+ -+Please note that keys are set to the valid YAML string ``---`` as it is not -+possible remove an existing key from the guestinfo key-space. A key's analogous -+encoding property will be set to a single white-space character, causing the -+datasource to treat the actual key value as plain-text, thereby loading it as -+an empty YAML doc (hence the aforementioned ``---``\ ). -+ -+Reading the local IP addresses -+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -+ -+This datasource automatically discovers the local IPv4 and IPv6 addresses for -+a guest operating system based on the default routes. However, when inspecting -+a VM externally, it's not possible to know what the *default* IP address is for -+the guest OS. That's why this datasource sets the discovered, local IPv4 and -+IPv6 addresses back in the guestinfo namespace as the following keys: -+ -+ -+* ``guestinfo.local-ipv4`` -+* ``guestinfo.local-ipv6`` -+ -+It is possible that a host may not have any default, local IP addresses. It's -+also possible the reported, local addresses are link-local addresses. But these -+two keys may be used to discover what this datasource determined were the local -+IPv4 and IPv6 addresses for a host. -+ -+Waiting on the network -+^^^^^^^^^^^^^^^^^^^^^^ -+ -+Sometimes cloud-init may bring up the network, but it will not finish coming -+online before the datasource's ``setup`` function is called, resulting in an -+``/var/run/cloud-init/instance-data.json`` file that does not have the correct -+network information. It is possible to instruct the datasource to wait until an -+IPv4 or IPv6 address is available before writing the instance data with the -+following metadata properties: -+ -+.. code-block:: yaml -+ -+ wait-on-network: -+ ipv4: true -+ ipv6: true -+ -+If either of the above values are true, then the datasource will sleep for a -+second, check the network status, and repeat until one or both addresses from -+the specified families are available. -+ -+Walkthrough -+----------- -+ -+The following series of steps is a demonstration on how to configure a VM with -+this datasource: -+ -+ -+#. Create the metadata file for the VM. Save the following YAML to a file named -+ ``metadata.yaml``\ : -+ -+ .. code-block:: yaml -+ -+ instance-id: cloud-vm -+ local-hostname: cloud-vm -+ network: -+ version: 2 -+ ethernets: -+ nics: -+ match: -+ name: ens* -+ dhcp4: yes -+ -+#. Create the userdata file ``userdata.yaml``\ : -+ -+ .. code-block:: yaml -+ -+ #cloud-config -+ -+ users: -+ - default -+ - name: akutz -+ primary_group: akutz -+ sudo: ALL=(ALL) NOPASSWD:ALL -+ groups: sudo, wheel -+ ssh_import_id: None -+ lock_passwd: true -+ ssh_authorized_keys: -+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDE0c5FczvcGSh/tG4iw+Fhfi/O5/EvUM/96js65tly4++YTXK1d9jcznPS5ruDlbIZ30oveCBd3kT8LLVFwzh6hepYTf0YmCTpF4eDunyqmpCXDvVscQYRXyasEm5olGmVe05RrCJSeSShAeptv4ueIn40kZKOghinGWLDSZG4+FFfgrmcMCpx5YSCtX2gvnEYZJr0czt4rxOZuuP7PkJKgC/mt2PcPjooeX00vAj81jjU2f3XKrjjz2u2+KIt9eba+vOQ6HiC8c2IzRkUAJ5i1atLy8RIbejo23+0P4N2jjk17QySFOVHwPBDTYb0/0M/4ideeU74EN/CgVsvO6JrLsPBR4dojkV5qNbMNxIVv5cUwIy2ThlLgqpNCeFIDLCWNZEFKlEuNeSQ2mPtIO7ETxEL2Cz5y/7AIuildzYMc6wi2bofRC8HmQ7rMXRWdwLKWsR0L7SKjHblIwarxOGqLnUI+k2E71YoP7SZSlxaKi17pqkr0OMCF+kKqvcvHAQuwGqyumTEWOlH6TCx1dSPrW+pVCZSHSJtSTfDW2uzL6y8k10MT06+pVunSrWo5LHAXcS91htHV1M1UrH/tZKSpjYtjMb5+RonfhaFRNzvj7cCE1f3Kp8UVqAdcGBTtReoE8eRUT63qIxjw03a7VwAyB2w+9cu1R9/vAo8SBeRqw== sakutz@gmail.com -+ -+#. Please note this step requires that the VM be powered off. All of the -+ commands below use the VMware CLI tool, `govc <https://github.com/vmware/govmomi/blob/master/govc>`_. -+ -+ Go ahead and assign the path to the VM to the environment variable ``VM``\ : -+ -+ .. code-block:: shell -+ -+ export VM="/inventory/path/to/the/vm" -+ -+#. Power off the VM: -+ -+ .. raw:: html -+ -+ <hr /> -+ -+ ⚠️ <strong>First Boot Mode</strong> -+ -+ To ensure the next power-on operation results in a first-boot scenario for -+ cloud-init, it may be necessary to run the following command just before -+ powering off the VM: -+ -+ .. code-block:: bash -+ -+ cloud-init clean -+ -+ Otherwise cloud-init may not run in first-boot mode. For more information -+ on how the boot mode is determined, please see the -+ `First Boot Documentation <../boot.html#first-boot-determination>`_. -+ -+ .. raw:: html -+ -+ <hr /> -+ -+ .. code-block:: shell -+ -+ govc vm.power -off "${VM}" -+ -+#. -+ Export the environment variables that contain the cloud-init metadata and -+ userdata: -+ -+ .. code-block:: shell -+ -+ export METADATA=$(gzip -c9 <metadata.yaml | { base64 -w0 2>/dev/null || base64; }) \ -+ USERDATA=$(gzip -c9 <userdata.yaml | { base64 -w0 2>/dev/null || base64; }) -+ -+#. -+ Assign the metadata and userdata to the VM: -+ -+ .. code-block:: shell -+ -+ govc vm.change -vm "${VM}" \ -+ -e guestinfo.metadata="${METADATA}" \ -+ -e guestinfo.metadata.encoding="gzip+base64" \ -+ -e guestinfo.userdata="${USERDATA}" \ -+ -e guestinfo.userdata.encoding="gzip+base64" -+ -+ Please note the above commands include specifying the encoding for the -+ properties. This is important as it informs the datasource how to decode -+ the data for cloud-init. Valid values for ``metadata.encoding`` and -+ ``userdata.encoding`` include: -+ -+ -+ * ``base64`` -+ * ``gzip+base64`` -+ -+#. -+ Power on the VM: -+ -+ .. code-block:: shell -+ -+ govc vm.power -vm "${VM}" -on -+ -+If all went according to plan, the CentOS box is: -+ -+* Locked down, allowing SSH access only for the user in the userdata -+* Configured for a dynamic IP address via DHCP -+* Has a hostname of ``cloud-vm`` -+ -+Examples -+-------- -+ -+This section reviews common configurations: -+ -+Setting the hostname -+^^^^^^^^^^^^^^^^^^^^ -+ -+The hostname is set by way of the metadata key ``local-hostname``. -+ -+Setting the instance ID -+^^^^^^^^^^^^^^^^^^^^^^^ -+ -+The instance ID may be set by way of the metadata key ``instance-id``. However, -+if this value is absent then then the instance ID is read from the file -+``/sys/class/dmi/id/product_uuid``. -+ -+Providing public SSH keys -+^^^^^^^^^^^^^^^^^^^^^^^^^ -+ -+The public SSH keys may be set by way of the metadata key ``public-keys-data``. -+Each newline-terminated string will be interpreted as a separate SSH public -+key, which will be placed in distro's default user's -+``~/.ssh/authorized_keys``. If the value is empty or absent, then nothing will -+be written to ``~/.ssh/authorized_keys``. -+ -+Configuring the network -+^^^^^^^^^^^^^^^^^^^^^^^ -+ -+The network is configured by setting the metadata key ``network`` with a value -+consistent with Network Config Versions -+`1 <../network-config-format-v1.html>`_ or -+`2 <../network-config-format-v2.html>`_\ , depending on the Linux -+distro's version of cloud-init. -+ -+The metadata key ``network.encoding`` may be used to indicate the format of -+the metadata key "network". Valid encodings are ``base64`` and ``gzip+base64``. -diff --git a/requirements.txt b/requirements.txt -index 5817da3b..41d01d62 100644 ---- a/requirements.txt -+++ b/requirements.txt -@@ -32,3 +32,12 @@ jsonpatch - - # For validating cloud-config sections per schema definitions - jsonschema -+ -+# Used by DataSourceVMware to inspect the host's network configuration during -+# the "setup()" function. -+# -+# This allows a host that uses DHCP to bring up the network during BootLocal -+# and still participate in instance-data by gathering the network in detail at -+# runtime and merge that information into the metadata and repersist that to -+# disk. -+netifaces>=0.10.9 -diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py -index 5912f7ee..475a2cf8 100644 ---- a/tests/unittests/test_datasource/test_common.py -+++ b/tests/unittests/test_datasource/test_common.py -@@ -28,6 +28,7 @@ from cloudinit.sources import ( - DataSourceScaleway as Scaleway, - DataSourceSmartOS as SmartOS, - DataSourceUpCloud as UpCloud, -+ DataSourceVMware as VMware, - ) - from cloudinit.sources import DataSourceNone as DSNone - -@@ -50,6 +51,7 @@ DEFAULT_LOCAL = [ - RbxCloud.DataSourceRbxCloud, - Scaleway.DataSourceScaleway, - UpCloud.DataSourceUpCloudLocal, -+ VMware.DataSourceVMware, - ] - - DEFAULT_NETWORK = [ -@@ -66,6 +68,7 @@ DEFAULT_NETWORK = [ - OpenStack.DataSourceOpenStack, - OVF.DataSourceOVFNet, - UpCloud.DataSourceUpCloud, -+ VMware.DataSourceVMware, - ] - - -diff --git a/tests/unittests/test_datasource/test_vmware.py b/tests/unittests/test_datasource/test_vmware.py -new file mode 100644 -index 00000000..597db7c8 ---- /dev/null -+++ b/tests/unittests/test_datasource/test_vmware.py -@@ -0,0 +1,377 @@ -+# Copyright (c) 2021 VMware, Inc. All Rights Reserved. -+# -+# Authors: Andrew Kutz <akutz@vmware.com> -+# -+# This file is part of cloud-init. See LICENSE file for license information. -+ -+import base64 -+import gzip -+from cloudinit import dmi, helpers, safeyaml -+from cloudinit import settings -+from cloudinit.sources import DataSourceVMware -+from cloudinit.tests.helpers import ( -+ mock, -+ CiTestCase, -+ FilesystemMockingTestCase, -+ populate_dir, -+) -+ -+import os -+ -+PRODUCT_NAME_FILE_PATH = "/sys/class/dmi/id/product_name" -+PRODUCT_NAME = "VMware7,1" -+PRODUCT_UUID = "82343CED-E4C7-423B-8F6B-0D34D19067AB" -+REROOT_FILES = { -+ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, -+ PRODUCT_NAME_FILE_PATH: PRODUCT_NAME, -+} -+ -+VMW_MULTIPLE_KEYS = [ -+ "ssh-rsa AAAAB3NzaC1yc2EAAAA... test1@vmw.com", -+ "ssh-rsa AAAAB3NzaC1yc2EAAAA... test2@vmw.com", -+] -+VMW_SINGLE_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAA... test@vmw.com" -+ -+VMW_METADATA_YAML = """instance-id: cloud-vm -+local-hostname: cloud-vm -+network: -+ version: 2 -+ ethernets: -+ nics: -+ match: -+ name: ens* -+ dhcp4: yes -+""" -+ -+VMW_USERDATA_YAML = """## template: jinja -+#cloud-config -+users: -+- default -+""" -+ -+VMW_VENDORDATA_YAML = """## template: jinja -+#cloud-config -+runcmd: -+- echo "Hello, world." -+""" -+ -+ -+class TestDataSourceVMware(CiTestCase): -+ """ -+ Test common functionality that is not transport specific. -+ """ -+ -+ def setUp(self): -+ super(TestDataSourceVMware, self).setUp() -+ self.tmp = self.tmp_dir() -+ -+ def test_no_data_access_method(self): -+ ds = get_ds(self.tmp) -+ ds.vmware_rpctool = None -+ ret = ds.get_data() -+ self.assertFalse(ret) -+ -+ def test_get_host_info(self): -+ host_info = DataSourceVMware.get_host_info() -+ self.assertTrue(host_info) -+ self.assertTrue(host_info["hostname"]) -+ self.assertTrue(host_info["local-hostname"]) -+ self.assertTrue(host_info["local_hostname"]) -+ self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4]) -+ -+ -+class TestDataSourceVMwareEnvVars(FilesystemMockingTestCase): -+ """ -+ Test the envvar transport. -+ """ -+ -+ def setUp(self): -+ super(TestDataSourceVMwareEnvVars, self).setUp() -+ self.tmp = self.tmp_dir() -+ os.environ[DataSourceVMware.VMX_GUESTINFO] = "1" -+ self.create_system_files() -+ -+ def tearDown(self): -+ del os.environ[DataSourceVMware.VMX_GUESTINFO] -+ return super(TestDataSourceVMwareEnvVars, self).tearDown() -+ -+ def create_system_files(self): -+ rootd = self.tmp_dir() -+ populate_dir( -+ rootd, -+ { -+ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, -+ }, -+ ) -+ self.assertTrue(self.reRoot(rootd)) -+ -+ def assert_get_data_ok(self, m_fn, m_fn_call_count=6): -+ ds = get_ds(self.tmp) -+ ds.vmware_rpctool = None -+ ret = ds.get_data() -+ self.assertTrue(ret) -+ self.assertEqual(m_fn_call_count, m_fn.call_count) -+ self.assertEqual( -+ ds.data_access_method, DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR -+ ) -+ return ds -+ -+ def assert_metadata(self, metadata, m_fn, m_fn_call_count=6): -+ ds = self.assert_get_data_ok(m_fn, m_fn_call_count) -+ assert_metadata(self, ds, metadata) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_subplatform(self, m_fn): -+ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] -+ ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ self.assertEqual( -+ ds.subplatform, -+ "%s (%s)" -+ % ( -+ DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR, -+ DataSourceVMware.get_guestinfo_envvar_key_name("metadata"), -+ ), -+ ) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_metadata_only(self, m_fn): -+ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_userdata_only(self, m_fn): -+ m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_vendordata_only(self, m_fn): -+ m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_metadata_base64(self, m_fn): -+ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) -+ m_fn.side_effect = [data, "base64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_metadata_b64(self, m_fn): -+ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) -+ m_fn.side_effect = [data, "b64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_metadata_gzip_base64(self, m_fn): -+ data = VMW_METADATA_YAML.encode("utf-8") -+ data = gzip.compress(data) -+ data = base64.b64encode(data) -+ m_fn.side_effect = [data, "gzip+base64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_get_data_metadata_gz_b64(self, m_fn): -+ data = VMW_METADATA_YAML.encode("utf-8") -+ data = gzip.compress(data) -+ data = base64.b64encode(data) -+ m_fn.side_effect = [data, "gz+b64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_metadata_single_ssh_key(self, m_fn): -+ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) -+ metadata["public_keys"] = VMW_SINGLE_KEY -+ metadata_yaml = safeyaml.dumps(metadata) -+ m_fn.side_effect = [metadata_yaml, "", "", ""] -+ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) -+ -+ @mock.patch( -+ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" -+ ) -+ def test_metadata_multiple_ssh_keys(self, m_fn): -+ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) -+ metadata["public_keys"] = VMW_MULTIPLE_KEYS -+ metadata_yaml = safeyaml.dumps(metadata) -+ m_fn.side_effect = [metadata_yaml, "", "", ""] -+ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) -+ -+ -+class TestDataSourceVMwareGuestInfo(FilesystemMockingTestCase): -+ """ -+ Test the guestinfo transport on a VMware platform. -+ """ -+ -+ def setUp(self): -+ super(TestDataSourceVMwareGuestInfo, self).setUp() -+ self.tmp = self.tmp_dir() -+ self.create_system_files() -+ -+ def create_system_files(self): -+ rootd = self.tmp_dir() -+ populate_dir( -+ rootd, -+ { -+ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, -+ PRODUCT_NAME_FILE_PATH: PRODUCT_NAME, -+ }, -+ ) -+ self.assertTrue(self.reRoot(rootd)) -+ -+ def assert_get_data_ok(self, m_fn, m_fn_call_count=6): -+ ds = get_ds(self.tmp) -+ ds.vmware_rpctool = "vmware-rpctool" -+ ret = ds.get_data() -+ self.assertTrue(ret) -+ self.assertEqual(m_fn_call_count, m_fn.call_count) -+ self.assertEqual( -+ ds.data_access_method, -+ DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, -+ ) -+ return ds -+ -+ def assert_metadata(self, metadata, m_fn, m_fn_call_count=6): -+ ds = self.assert_get_data_ok(m_fn, m_fn_call_count) -+ assert_metadata(self, ds, metadata) -+ -+ def test_ds_valid_on_vmware_platform(self): -+ system_type = dmi.read_dmi_data("system-product-name") -+ self.assertEqual(system_type, PRODUCT_NAME) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_subplatform(self, m_fn): -+ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] -+ ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ self.assertEqual( -+ ds.subplatform, -+ "%s (%s)" -+ % ( -+ DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, -+ DataSourceVMware.get_guestinfo_key_name("metadata"), -+ ), -+ ) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_data_userdata_only(self, m_fn): -+ m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_data_vendordata_only(self, m_fn): -+ m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_metadata_single_ssh_key(self, m_fn): -+ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) -+ metadata["public_keys"] = VMW_SINGLE_KEY -+ metadata_yaml = safeyaml.dumps(metadata) -+ m_fn.side_effect = [metadata_yaml, "", "", ""] -+ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_metadata_multiple_ssh_keys(self, m_fn): -+ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) -+ metadata["public_keys"] = VMW_MULTIPLE_KEYS -+ metadata_yaml = safeyaml.dumps(metadata) -+ m_fn.side_effect = [metadata_yaml, "", "", ""] -+ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_data_metadata_base64(self, m_fn): -+ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) -+ m_fn.side_effect = [data, "base64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_data_metadata_b64(self, m_fn): -+ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) -+ m_fn.side_effect = [data, "b64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_data_metadata_gzip_base64(self, m_fn): -+ data = VMW_METADATA_YAML.encode("utf-8") -+ data = gzip.compress(data) -+ data = base64.b64encode(data) -+ m_fn.side_effect = [data, "gzip+base64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_get_data_metadata_gz_b64(self, m_fn): -+ data = VMW_METADATA_YAML.encode("utf-8") -+ data = gzip.compress(data) -+ data = base64.b64encode(data) -+ m_fn.side_effect = [data, "gz+b64", "", ""] -+ self.assert_get_data_ok(m_fn, m_fn_call_count=4) -+ -+ -+class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase): -+ """ -+ Test the guestinfo transport on a non-VMware platform. -+ """ -+ -+ def setUp(self): -+ super(TestDataSourceVMwareGuestInfo_InvalidPlatform, self).setUp() -+ self.tmp = self.tmp_dir() -+ self.create_system_files() -+ -+ def create_system_files(self): -+ rootd = self.tmp_dir() -+ populate_dir( -+ rootd, -+ { -+ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, -+ }, -+ ) -+ self.assertTrue(self.reRoot(rootd)) -+ -+ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") -+ def test_ds_invalid_on_non_vmware_platform(self, m_fn): -+ system_type = dmi.read_dmi_data("system-product-name") -+ self.assertEqual(system_type, None) -+ -+ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] -+ ds = get_ds(self.tmp) -+ ds.vmware_rpctool = "vmware-rpctool" -+ ret = ds.get_data() -+ self.assertFalse(ret) -+ -+ -+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()) -+ -+ expected_public_keys = metadata.get("public_keys") -+ if not isinstance(expected_public_keys, list): -+ expected_public_keys = [expected_public_keys] -+ -+ test_obj.assertEqual(expected_public_keys, ds.get_public_ssh_keys()) -+ test_obj.assertIsInstance(ds.get_public_ssh_keys(), list) -+ -+ -+def get_ds(temp_dir): -+ ds = DataSourceVMware.DataSourceVMware( -+ settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": temp_dir}) -+ ) -+ ds.vmware_rpctool = "vmware-rpctool" -+ return ds -+ -+ -+# vi: ts=4 expandtab -diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py -index 1d8aaf18..8617d7bd 100644 ---- a/tests/unittests/test_ds_identify.py -+++ b/tests/unittests/test_ds_identify.py -@@ -649,6 +649,50 @@ class TestDsIdentify(DsIdentifyBase): - """EC2: bobrightbox.com in product_serial is not brightbox'""" - self._test_ds_not_found('Ec2-E24Cloud-negative') - -+ def test_vmware_no_valid_transports(self): -+ """VMware: no valid transports""" -+ self._test_ds_not_found('VMware-NoValidTransports') -+ -+ def test_vmware_envvar_no_data(self): -+ """VMware: envvar transport no data""" -+ self._test_ds_not_found('VMware-EnvVar-NoData') -+ -+ def test_vmware_envvar_no_virt_id(self): -+ """VMware: envvar transport success if no virt id""" -+ self._test_ds_found('VMware-EnvVar-NoVirtID') -+ -+ def test_vmware_envvar_activated_by_metadata(self): -+ """VMware: envvar transport activated by metadata""" -+ self._test_ds_found('VMware-EnvVar-Metadata') -+ -+ def test_vmware_envvar_activated_by_userdata(self): -+ """VMware: envvar transport activated by userdata""" -+ self._test_ds_found('VMware-EnvVar-Userdata') -+ -+ def test_vmware_envvar_activated_by_vendordata(self): -+ """VMware: envvar transport activated by vendordata""" -+ self._test_ds_found('VMware-EnvVar-Vendordata') -+ -+ def test_vmware_guestinfo_no_data(self): -+ """VMware: guestinfo transport no data""" -+ self._test_ds_not_found('VMware-GuestInfo-NoData') -+ -+ def test_vmware_guestinfo_no_virt_id(self): -+ """VMware: guestinfo transport fails if no virt id""" -+ self._test_ds_not_found('VMware-GuestInfo-NoVirtID') -+ -+ def test_vmware_guestinfo_activated_by_metadata(self): -+ """VMware: guestinfo transport activated by metadata""" -+ self._test_ds_found('VMware-GuestInfo-Metadata') -+ -+ def test_vmware_guestinfo_activated_by_userdata(self): -+ """VMware: guestinfo transport activated by userdata""" -+ self._test_ds_found('VMware-GuestInfo-Userdata') -+ -+ def test_vmware_guestinfo_activated_by_vendordata(self): -+ """VMware: guestinfo transport activated by vendordata""" -+ self._test_ds_found('VMware-GuestInfo-Vendordata') -+ - - class TestBSDNoSys(DsIdentifyBase): - """Test *BSD code paths -@@ -1136,7 +1180,240 @@ VALID_CFG = { - 'Ec2-E24Cloud-negative': { - 'ds': 'Ec2', - 'files': {P_SYS_VENDOR: 'e24cloudyday\n'}, -- } -+ }, -+ 'VMware-NoValidTransports': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-EnvVar-NoData': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-EnvVar-NoVirtID': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ ], -+ }, -+ 'VMware-EnvVar-Metadata': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-EnvVar-Userdata': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-EnvVar-Vendordata': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo', -+ 'ret': 0, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', -+ 'ret': 0, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-GuestInfo-NoData': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_rpctool', -+ 'ret': 0, -+ 'out': '/usr/bin/vmware-rpctool', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_metadata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-GuestInfo-NoVirtID': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_rpctool', -+ 'ret': 0, -+ 'out': '/usr/bin/vmware-rpctool', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_metadata', -+ 'ret': 0, -+ 'out': '---', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ ], -+ }, -+ 'VMware-GuestInfo-Metadata': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_rpctool', -+ 'ret': 0, -+ 'out': '/usr/bin/vmware-rpctool', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_metadata', -+ 'ret': 0, -+ 'out': '---', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-GuestInfo-Userdata': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_rpctool', -+ 'ret': 0, -+ 'out': '/usr/bin/vmware-rpctool', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_metadata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_userdata', -+ 'ret': 0, -+ 'out': '---', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_vendordata', -+ 'ret': 1, -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, -+ 'VMware-GuestInfo-Vendordata': { -+ 'ds': 'VMware', -+ 'mocks': [ -+ { -+ 'name': 'vmware_has_rpctool', -+ 'ret': 0, -+ 'out': '/usr/bin/vmware-rpctool', -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_metadata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_userdata', -+ 'ret': 1, -+ }, -+ { -+ 'name': 'vmware_rpctool_guestinfo_vendordata', -+ 'ret': 0, -+ 'out': '---', -+ }, -+ MOCK_VIRT_IS_VMWARE, -+ ], -+ }, - } - - # vi: ts=4 expandtab -diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers -index 689d7902..cbfa883c 100644 ---- a/tools/.github-cla-signers -+++ b/tools/.github-cla-signers -@@ -1,5 +1,6 @@ - ader1990 - ajmyyra -+akutz - AlexBaranowski - Aman306 - andrewbogott -diff --git a/tools/ds-identify b/tools/ds-identify -index 2f2486f7..c01eae3d 100755 ---- a/tools/ds-identify -+++ b/tools/ds-identify -@@ -125,7 +125,7 @@ DI_DSNAME="" - # be searched if there is no setting found in config. - DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ - CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ --OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud" -+OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud VMware" - DI_DSLIST="" - DI_MODE="" - DI_ON_FOUND="" -@@ -1350,6 +1350,80 @@ dscheck_IBMCloud() { - return ${DS_NOT_FOUND} - } - -+vmware_has_envvar_vmx_guestinfo() { -+ [ -n "${VMX_GUESTINFO:-}" ] -+} -+ -+vmware_has_envvar_vmx_guestinfo_metadata() { -+ [ -n "${VMX_GUESTINFO_METADATA:-}" ] -+} -+ -+vmware_has_envvar_vmx_guestinfo_userdata() { -+ [ -n "${VMX_GUESTINFO_USERDATA:-}" ] -+} -+ -+vmware_has_envvar_vmx_guestinfo_vendordata() { -+ [ -n "${VMX_GUESTINFO_VENDORDATA:-}" ] -+} -+ -+vmware_has_rpctool() { -+ command -v vmware-rpctool >/dev/null 2>&1 -+} -+ -+vmware_rpctool_guestinfo_metadata() { -+ vmware-rpctool "info-get guestinfo.metadata" -+} -+ -+vmware_rpctool_guestinfo_userdata() { -+ vmware-rpctool "info-get guestinfo.userdata" -+} -+ -+vmware_rpctool_guestinfo_vendordata() { -+ vmware-rpctool "info-get guestinfo.vendordata" -+} -+ -+dscheck_VMware() { -+ # Checks to see if there is valid data for the VMware datasource. -+ # The data transports are checked in the following order: -+ # -+ # * envvars -+ # * guestinfo -+ # -+ # Please note when updating this function with support for new data -+ # transports, the order should match the order in the _get_data -+ # function from the file DataSourceVMware.py. -+ -+ # Check to see if running in a container and the VMware -+ # datasource is configured via environment variables. -+ if vmware_has_envvar_vmx_guestinfo; then -+ if vmware_has_envvar_vmx_guestinfo_metadata || \ -+ vmware_has_envvar_vmx_guestinfo_userdata || \ -+ vmware_has_envvar_vmx_guestinfo_vendordata; then -+ return "${DS_FOUND}" -+ fi -+ fi -+ -+ # Do not proceed unless the detected platform is VMware. -+ if [ ! "${DI_VIRT}" = "vmware" ]; then -+ return "${DS_NOT_FOUND}" -+ fi -+ -+ # Do not proceed if the vmware-rpctool command is not present. -+ if ! vmware_has_rpctool; then -+ return "${DS_NOT_FOUND}" -+ fi -+ -+ # Activate the VMware datasource only if any of the fields used -+ # by the datasource are present in the guestinfo table. -+ if { vmware_rpctool_guestinfo_metadata || \ -+ vmware_rpctool_guestinfo_userdata || \ -+ vmware_rpctool_guestinfo_vendordata; } >/dev/null 2>&1; then -+ return "${DS_FOUND}" -+ fi -+ -+ return "${DS_NOT_FOUND}" -+} -+ - collect_info() { - read_uname_info - read_virt --- -2.27.0 - diff --git a/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch b/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch deleted file mode 100644 index d4ec623..0000000 --- a/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch +++ /dev/null @@ -1,474 +0,0 @@ -From 7bd016008429f0a18393a070d88e669f3ed89caa Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Fri, 11 Feb 2022 14:37:46 +0100 -Subject: [PATCH] Fix IPv6 netmask format for sysconfig (#1215) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 48: Fix IPv6 netmask format for sysconfig (#1215) -RH-Commit: [1/1] 4c940bbcf85dba1fce9f4acb9fc7820c0d7777f6 -RH-Bugzilla: 2046540 -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> -RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> - -commit b97a30f0a05c1dea918c46ca9c05c869d15fe2d5 -Author: Harald <hjensas@redhat.com> -Date: Tue Feb 8 15:49:00 2022 +0100 - - Fix IPv6 netmask format for sysconfig (#1215) - - This change converts the IPv6 netmask from the network_data.json[1] - format to the CIDR style, <IPv6_addr>/<prefix>. - - Using an IPv6 address like ffff:ffff:ffff:ffff:: does not work with - NetworkManager, nor networkscripts. - - NetworkManager will ignore the route, logging: - ifcfg-rh: ignoring invalid route at \ - "::/:: via fd00:fd00:fd00:2::fffe dev $DEV" \ - (/etc/sysconfig/network-scripts/route6-$DEV:3): \ - Argument for "::/::" is not ADDR/PREFIX format - - Similarly if using networkscripts, ip route fail with error: - Error: inet6 prefix is expected rather than \ - "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::". - - Also a bit of refactoring ... - - cloudinit.net.sysconfig.Route.to_string: - * Move a couple of lines around to reduce repeated code. - * if "ADDRESS" not in key -> continute, so that the - code block following it can be de-indented. - cloudinit.net.network_state: - * Refactors the ipv4_mask_to_net_prefix, ipv6_mask_to_net_prefix - removes mask_to_net_prefix methods. Utilize ipaddress library to - do some of the heavy lifting. - - LP: #1959148 - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/net/__init__.py | 7 +- - cloudinit/net/network_state.py | 103 +++++++----------- - cloudinit/net/sysconfig.py | 91 ++++++++++------ - cloudinit/sources/DataSourceOpenNebula.py | 2 +- - .../sources/helpers/vmware/imc/config_nic.py | 4 +- - tests/unittests/test_net.py | 78 ++++++++++++- - 6 files changed, 176 insertions(+), 109 deletions(-) - -diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py -index 003efa2a..12bf64de 100644 ---- a/cloudinit/net/__init__.py -+++ b/cloudinit/net/__init__.py -@@ -14,7 +14,7 @@ import re - - from cloudinit import subp - from cloudinit import util --from cloudinit.net.network_state import mask_to_net_prefix -+from cloudinit.net.network_state import ipv4_mask_to_net_prefix - from cloudinit.url_helper import UrlError, readurl - - LOG = logging.getLogger(__name__) -@@ -1048,10 +1048,11 @@ class EphemeralIPv4Network(object): - 'Cannot init network on {0} with {1}/{2} and bcast {3}'.format( - interface, ip, prefix_or_mask, broadcast)) - try: -- self.prefix = mask_to_net_prefix(prefix_or_mask) -+ self.prefix = ipv4_mask_to_net_prefix(prefix_or_mask) - except ValueError as e: - raise ValueError( -- 'Cannot setup network: {0}'.format(e) -+ "Cannot setup network, invalid prefix or " -+ "netmask: {0}".format(e) - ) from e - - self.connectivity_url = connectivity_url -diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py -index e8bf9e39..2768ef94 100644 ---- a/cloudinit/net/network_state.py -+++ b/cloudinit/net/network_state.py -@@ -6,6 +6,7 @@ - - import copy - import functools -+import ipaddress - import logging - import socket - import struct -@@ -872,12 +873,18 @@ def _normalize_net_keys(network, address_keys=()): - try: - prefix = int(maybe_prefix) - except ValueError: -- # this supports input of <address>/255.255.255.0 -- prefix = mask_to_net_prefix(maybe_prefix) -- elif netmask: -- prefix = mask_to_net_prefix(netmask) -- elif 'prefix' in net: -- prefix = int(net['prefix']) -+ if ipv6: -+ # this supports input of ffff:ffff:ffff:: -+ prefix = ipv6_mask_to_net_prefix(maybe_prefix) -+ else: -+ # this supports input of 255.255.255.0 -+ prefix = ipv4_mask_to_net_prefix(maybe_prefix) -+ elif netmask and not ipv6: -+ prefix = ipv4_mask_to_net_prefix(netmask) -+ elif netmask and ipv6: -+ prefix = ipv6_mask_to_net_prefix(netmask) -+ elif "prefix" in net: -+ prefix = int(net["prefix"]) - else: - prefix = 64 if ipv6 else 24 - -@@ -972,72 +979,42 @@ def ipv4_mask_to_net_prefix(mask): - str(24) => 24 - "24" => 24 - """ -- if isinstance(mask, int): -- return mask -- if isinstance(mask, str): -- try: -- return int(mask) -- except ValueError: -- pass -- else: -- raise TypeError("mask '%s' is not a string or int") -- -- if '.' not in mask: -- raise ValueError("netmask '%s' does not contain a '.'" % mask) -- -- toks = mask.split(".") -- if len(toks) != 4: -- raise ValueError("netmask '%s' had only %d parts" % (mask, len(toks))) -- -- return sum([bin(int(x)).count('1') for x in toks]) -+ return ipaddress.ip_network(f"0.0.0.0/{mask}").prefixlen - - - def ipv6_mask_to_net_prefix(mask): - """Convert an ipv6 netmask (very uncommon) or prefix (64) to prefix. - -- If 'mask' is an integer or string representation of one then -- int(mask) will be returned. -+ If the input is already an integer or a string representation of -+ an integer, then int(mask) will be returned. -+ "ffff:ffff:ffff::" => 48 -+ "48" => 48 - """ -- -- if isinstance(mask, int): -- return mask -- if isinstance(mask, str): -- try: -- return int(mask) -- except ValueError: -- pass -- else: -- raise TypeError("mask '%s' is not a string or int") -- -- if ':' not in mask: -- raise ValueError("mask '%s' does not have a ':'") -- -- bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00, -- 0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc, -- 0xfffe, 0xffff] -- prefix = 0 -- for word in mask.split(':'): -- if not word or int(word, 16) == 0: -- break -- prefix += bitCount.index(int(word, 16)) -- -- return prefix -- -- --def mask_to_net_prefix(mask): -- """Return the network prefix for the netmask provided. -- -- Supports ipv4 or ipv6 netmasks.""" - try: -- # if 'mask' is a prefix that is an integer. -- # then just return it. -- return int(mask) -+ # In the case the mask is already a prefix -+ prefixlen = ipaddress.ip_network(f"::/{mask}").prefixlen -+ return prefixlen - except ValueError: -+ # ValueError means mask is an IPv6 address representation and need -+ # conversion. - pass -- if is_ipv6_addr(mask): -- return ipv6_mask_to_net_prefix(mask) -- else: -- return ipv4_mask_to_net_prefix(mask) -+ -+ netmask = ipaddress.ip_address(mask) -+ mask_int = int(netmask) -+ # If the mask is all zeroes, just return it -+ if mask_int == 0: -+ return mask_int -+ -+ trailing_zeroes = min( -+ ipaddress.IPV6LENGTH, (~mask_int & (mask_int - 1)).bit_length() -+ ) -+ leading_ones = mask_int >> trailing_zeroes -+ prefixlen = ipaddress.IPV6LENGTH - trailing_zeroes -+ all_ones = (1 << prefixlen) - 1 -+ if leading_ones != all_ones: -+ raise ValueError("Invalid network mask '%s'" % mask) -+ -+ return prefixlen - - - def mask_and_ipv4_to_bcast_addr(mask, ip): -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index d5440998..7ecbe1c3 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -12,6 +12,7 @@ from cloudinit import util - from cloudinit import subp - from cloudinit.distros.parsers import networkmanager_conf - from cloudinit.distros.parsers import resolv_conf -+from cloudinit.net import network_state - - from . import renderer - from .network_state import ( -@@ -171,43 +172,61 @@ class Route(ConfigMap): - # (because Route can contain a mix of IPv4 and IPv6) - reindex = -1 - for key in sorted(self._conf.keys()): -- if 'ADDRESS' in key: -- index = key.replace('ADDRESS', '') -- address_value = str(self._conf[key]) -- # only accept combinations: -- # if proto ipv6 only display ipv6 routes -- # if proto ipv4 only display ipv4 routes -- # do not add ipv6 routes if proto is ipv4 -- # do not add ipv4 routes if proto is ipv6 -- # (this array will contain a mix of ipv4 and ipv6) -- if proto == "ipv4" and not self.is_ipv6_route(address_value): -- netmask_value = str(self._conf['NETMASK' + index]) -- gateway_value = str(self._conf['GATEWAY' + index]) -- # increase IPv4 index -- reindex = reindex + 1 -- buf.write("%s=%s\n" % ('ADDRESS' + str(reindex), -- _quote_value(address_value))) -- buf.write("%s=%s\n" % ('GATEWAY' + str(reindex), -- _quote_value(gateway_value))) -- buf.write("%s=%s\n" % ('NETMASK' + str(reindex), -- _quote_value(netmask_value))) -- metric_key = 'METRIC' + index -- if metric_key in self._conf: -- metric_value = str(self._conf['METRIC' + index]) -- buf.write("%s=%s\n" % ('METRIC' + str(reindex), -- _quote_value(metric_value))) -- elif proto == "ipv6" and self.is_ipv6_route(address_value): -- netmask_value = str(self._conf['NETMASK' + index]) -- gateway_value = str(self._conf['GATEWAY' + index]) -- metric_value = ( -- 'metric ' + str(self._conf['METRIC' + index]) -- if 'METRIC' + index in self._conf else '') -+ if "ADDRESS" not in key: -+ continue -+ -+ index = key.replace("ADDRESS", "") -+ address_value = str(self._conf[key]) -+ netmask_value = str(self._conf["NETMASK" + index]) -+ gateway_value = str(self._conf["GATEWAY" + index]) -+ -+ # only accept combinations: -+ # if proto ipv6 only display ipv6 routes -+ # if proto ipv4 only display ipv4 routes -+ # do not add ipv6 routes if proto is ipv4 -+ # do not add ipv4 routes if proto is ipv6 -+ # (this array will contain a mix of ipv4 and ipv6) -+ if proto == "ipv4" and not self.is_ipv6_route(address_value): -+ # increase IPv4 index -+ reindex = reindex + 1 -+ buf.write( -+ "%s=%s\n" -+ % ("ADDRESS" + str(reindex), _quote_value(address_value)) -+ ) -+ buf.write( -+ "%s=%s\n" -+ % ("GATEWAY" + str(reindex), _quote_value(gateway_value)) -+ ) -+ buf.write( -+ "%s=%s\n" -+ % ("NETMASK" + str(reindex), _quote_value(netmask_value)) -+ ) -+ metric_key = "METRIC" + index -+ if metric_key in self._conf: -+ metric_value = str(self._conf["METRIC" + index]) - buf.write( -- "%s/%s via %s %s dev %s\n" % (address_value, -- netmask_value, -- gateway_value, -- metric_value, -- self._route_name)) -+ "%s=%s\n" -+ % ("METRIC" + str(reindex), _quote_value(metric_value)) -+ ) -+ elif proto == "ipv6" and self.is_ipv6_route(address_value): -+ prefix_value = network_state.ipv6_mask_to_net_prefix( -+ netmask_value -+ ) -+ metric_value = ( -+ "metric " + str(self._conf["METRIC" + index]) -+ if "METRIC" + index in self._conf -+ else "" -+ ) -+ buf.write( -+ "%s/%s via %s %s dev %s\n" -+ % ( -+ address_value, -+ prefix_value, -+ gateway_value, -+ metric_value, -+ self._route_name, -+ ) -+ ) - - return buf.getvalue() - -diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py -index 730ec586..e7980ab1 100644 ---- a/cloudinit/sources/DataSourceOpenNebula.py -+++ b/cloudinit/sources/DataSourceOpenNebula.py -@@ -233,7 +233,7 @@ class OpenNebulaNetwork(object): - # Set IPv4 address - devconf['addresses'] = [] - mask = self.get_mask(c_dev) -- prefix = str(net.mask_to_net_prefix(mask)) -+ prefix = str(net.ipv4_mask_to_net_prefix(mask)) - devconf['addresses'].append( - self.get_ip(c_dev, mac) + '/' + prefix) - -diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py -index 9cd2c0c0..3a45c67e 100644 ---- a/cloudinit/sources/helpers/vmware/imc/config_nic.py -+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py -@@ -9,7 +9,7 @@ import logging - import os - import re - --from cloudinit.net.network_state import mask_to_net_prefix -+from cloudinit.net.network_state import ipv4_mask_to_net_prefix - from cloudinit import subp - from cloudinit import util - -@@ -180,7 +180,7 @@ class NicConfigurator(object): - """ - route_list = [] - -- cidr = mask_to_net_prefix(netmask) -+ cidr = ipv4_mask_to_net_prefix(netmask) - - for gateway in gateways: - destination = "%s/%d" % (gen_subnet(gateway, netmask), cidr) -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index 14d3462f..a7f6a1f7 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -2025,10 +2025,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - routes: - - gateway: 2001:67c:1562:1 - network: 2001:67c:1 -- netmask: ffff:ffff:0 -+ netmask: "ffff:ffff::" - - gateway: 3001:67c:1562:1 - network: 3001:67c:1 -- netmask: ffff:ffff:0 -+ netmask: "ffff:ffff::" - metric: 10000 - """), - 'expected_netplan': textwrap.dedent(""" -@@ -2295,8 +2295,8 @@ iface bond0 inet6 static - 'route6-bond0': textwrap.dedent("""\ - # Created by cloud-init on instance boot automatically, do not edit. - # -- 2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1 dev bond0 -- 3001:67c:1/ffff:ffff:0 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:1562:1 metric 10000 dev bond0 - """), - 'route-bond0': textwrap.dedent("""\ - ADDRESS0=10.1.3.0 -@@ -3088,6 +3088,76 @@ USERCTL=no - renderer.render_network_state(ns, target=render_dir) - self.assertEqual([], os.listdir(render_dir)) - -+ def test_invalid_network_mask_ipv6(self): -+ net_json = { -+ "services": [{"type": "dns", "address": "172.19.0.12"}], -+ "networks": [ -+ { -+ "network_id": "public-ipv6", -+ "type": "ipv6", -+ "netmask": "", -+ "link": "tap1a81968a-79", -+ "routes": [ -+ { -+ "gateway": "2001:DB8::1", -+ "netmask": "ff:ff:ff:ff::", -+ "network": "2001:DB8:1::1", -+ }, -+ ], -+ "ip_address": "2001:DB8::10", -+ "id": "network1", -+ } -+ ], -+ "links": [ -+ { -+ "ethernet_mac_address": "fa:16:3e:ed:9a:59", -+ "mtu": None, -+ "type": "bridge", -+ "id": "tap1a81968a-79", -+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f", -+ }, -+ ], -+ } -+ macs = {"fa:16:3e:ed:9a:59": "eth0"} -+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs) -+ with self.assertRaises(ValueError): -+ network_state.parse_net_config_data(network_cfg, skip_broken=False) -+ -+ def test_invalid_network_mask_ipv4(self): -+ net_json = { -+ "services": [{"type": "dns", "address": "172.19.0.12"}], -+ "networks": [ -+ { -+ "network_id": "public-ipv4", -+ "type": "ipv4", -+ "netmask": "", -+ "link": "tap1a81968a-79", -+ "routes": [ -+ { -+ "gateway": "172.20.0.1", -+ "netmask": "255.234.255.0", -+ "network": "172.19.0.0", -+ }, -+ ], -+ "ip_address": "172.20.0.10", -+ "id": "network1", -+ } -+ ], -+ "links": [ -+ { -+ "ethernet_mac_address": "fa:16:3e:ed:9a:59", -+ "mtu": None, -+ "type": "bridge", -+ "id": "tap1a81968a-79", -+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f", -+ }, -+ ], -+ } -+ macs = {"fa:16:3e:ed:9a:59": "eth0"} -+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs) -+ with self.assertRaises(ValueError): -+ network_state.parse_net_config_data(network_cfg, skip_broken=False) -+ - def test_openstack_rendering_samples(self): - for os_sample in OS_SAMPLES: - render_dir = self.tmp_dir() --- -2.27.0 - diff --git a/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch b/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch deleted file mode 100644 index 6a9cfcc..0000000 --- a/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch +++ /dev/null @@ -1,262 +0,0 @@ -From 71989367e7a634fdd2af8ef58473975e0ef60464 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Sat, 21 Aug 2021 13:53:27 +0200 -Subject: [PATCH] Fix home permissions modified by ssh module (SC-338) (#984) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 29: Fix home permissions modified by ssh module (SC-338) (#984) -RH-Commit: [1/1] c409f2609b1d7e024eba77b55a196a4cafadd1d7 (eesposit/cloud-init) -RH-Bugzilla: 1995840 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -TESTED: By me and QA -BREW: 39178090 - -Fix home permissions modified by ssh module (SC-338) (#984) - -commit 7d3f5d750f6111c2716143364ea33486df67c927 -Author: James Falcon <therealfalcon@gmail.com> -Date: Fri Aug 20 17:09:49 2021 -0500 - - Fix home permissions modified by ssh module (SC-338) (#984) - - Fix home permissions modified by ssh module - - In #956, we updated the file and directory permissions for keys not in - the user's home directory. We also unintentionally modified the - permissions within the home directory as well. These should not change, - and this commit changes that back. - - LP: #1940233 - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/ssh_util.py | 35 ++++- - .../modules/test_ssh_keysfile.py | 132 +++++++++++++++--- - 2 files changed, 146 insertions(+), 21 deletions(-) - -diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py -index b8a3c8f7..9ccadf09 100644 ---- a/cloudinit/ssh_util.py -+++ b/cloudinit/ssh_util.py -@@ -321,23 +321,48 @@ def check_create_path(username, filename, strictmodes): - home_folder = os.path.dirname(user_pwent.pw_dir) - for directory in directories: - parent_folder += "/" + directory -- if home_folder.startswith(parent_folder): -+ -+ # security check, disallow symlinks in the AuthorizedKeysFile path. -+ if os.path.islink(parent_folder): -+ LOG.debug( -+ "Invalid directory. Symlink exists in path: %s", -+ parent_folder) -+ return False -+ -+ if os.path.isfile(parent_folder): -+ LOG.debug( -+ "Invalid directory. File exists in path: %s", -+ parent_folder) -+ return False -+ -+ if (home_folder.startswith(parent_folder) or -+ parent_folder == user_pwent.pw_dir): - continue - -- if not os.path.isdir(parent_folder): -+ if not os.path.exists(parent_folder): - # directory does not exist, and permission so far are good: - # create the directory, and make it accessible by everyone - # but owned by root, as it might be used by many users. - with util.SeLinuxGuard(parent_folder): -- os.makedirs(parent_folder, mode=0o755, exist_ok=True) -- util.chownbyid(parent_folder, root_pwent.pw_uid, -- root_pwent.pw_gid) -+ mode = 0o755 -+ uid = root_pwent.pw_uid -+ gid = root_pwent.pw_gid -+ if parent_folder.startswith(user_pwent.pw_dir): -+ mode = 0o700 -+ uid = user_pwent.pw_uid -+ gid = user_pwent.pw_gid -+ os.makedirs(parent_folder, mode=mode, exist_ok=True) -+ util.chownbyid(parent_folder, uid, gid) - - permissions = check_permissions(username, parent_folder, - filename, False, strictmodes) - if not permissions: - return False - -+ if os.path.islink(filename) or os.path.isdir(filename): -+ LOG.debug("%s is not a file!", filename) -+ return False -+ - # check the file - if not os.path.exists(filename): - # if file does not exist: we need to create it, since the -diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py -index f82d7649..3159feb9 100644 ---- a/tests/integration_tests/modules/test_ssh_keysfile.py -+++ b/tests/integration_tests/modules/test_ssh_keysfile.py -@@ -10,10 +10,10 @@ TEST_USER1_KEYS = get_test_rsa_keypair('test1') - TEST_USER2_KEYS = get_test_rsa_keypair('test2') - TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3') - --USERDATA = """\ -+_USERDATA = """\ - #cloud-config - bootcmd: -- - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config -+ - {bootcmd} - ssh_authorized_keys: - - {default} - users: -@@ -24,27 +24,17 @@ users: - - name: test_user2 - ssh_authorized_keys: - - {user2} --""".format( # noqa: E501 -+""".format( -+ bootcmd='{bootcmd}', - default=TEST_DEFAULT_KEYS.public_key, - user1=TEST_USER1_KEYS.public_key, - user2=TEST_USER2_KEYS.public_key, - ) - - --@pytest.mark.ubuntu --@pytest.mark.user_data(USERDATA) --def test_authorized_keys(client: IntegrationInstance): -- expected_keys = [ -- ('test_user1', '/home/test_user1/.ssh/authorized_keys2', -- TEST_USER1_KEYS), -- ('test_user2', '/home/test_user2/.ssh/authorized_keys2', -- TEST_USER2_KEYS), -- ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2', -- TEST_DEFAULT_KEYS), -- ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS), -- ] -- -+def common_verify(client, expected_keys): - for user, filename, keys in expected_keys: -+ # Ensure key is in the key file - contents = client.read_from_file(filename) - if user in ['ubuntu', 'root']: - # Our personal public key gets added by pycloudlib -@@ -83,3 +73,113 @@ def test_authorized_keys(client: IntegrationInstance): - look_for_keys=False, - allow_agent=False, - ) -+ -+ # Ensure we haven't messed with any /home permissions -+ # See LP: #1940233 -+ home_dir = '/home/{}'.format(user) -+ home_perms = '755' -+ if user == 'root': -+ home_dir = '/root' -+ home_perms = '700' -+ assert '{} {}'.format(user, home_perms) == client.execute( -+ 'stat -c "%U %a" {}'.format(home_dir) -+ ) -+ if client.execute("test -d {}/.ssh".format(home_dir)).ok: -+ assert '{} 700'.format(user) == client.execute( -+ 'stat -c "%U %a" {}/.ssh'.format(home_dir) -+ ) -+ assert '{} 600'.format(user) == client.execute( -+ 'stat -c "%U %a" {}'.format(filename) -+ ) -+ -+ # Also ensure ssh-keygen works as expected -+ client.execute('mkdir {}/.ssh'.format(home_dir)) -+ assert client.execute( -+ "ssh-keygen -b 2048 -t rsa -f {}/.ssh/id_rsa -q -N ''".format( -+ home_dir) -+ ).ok -+ assert client.execute('test -f {}/.ssh/id_rsa'.format(home_dir)) -+ assert client.execute('test -f {}/.ssh/id_rsa.pub'.format(home_dir)) -+ -+ assert 'root 755' == client.execute('stat -c "%U %a" /home') -+ -+ -+DEFAULT_KEYS_USERDATA = _USERDATA.format(bootcmd='""') -+ -+ -+@pytest.mark.ubuntu -+@pytest.mark.user_data(DEFAULT_KEYS_USERDATA) -+def test_authorized_keys_default(client: IntegrationInstance): -+ expected_keys = [ -+ ('test_user1', '/home/test_user1/.ssh/authorized_keys', -+ TEST_USER1_KEYS), -+ ('test_user2', '/home/test_user2/.ssh/authorized_keys', -+ TEST_USER2_KEYS), -+ ('ubuntu', '/home/ubuntu/.ssh/authorized_keys', -+ TEST_DEFAULT_KEYS), -+ ('root', '/root/.ssh/authorized_keys', TEST_DEFAULT_KEYS), -+ ] -+ common_verify(client, expected_keys) -+ -+ -+AUTHORIZED_KEYS2_USERDATA = _USERDATA.format(bootcmd=( -+ "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile " -+ "/etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' " -+ "/etc/ssh/sshd_config")) -+ -+ -+@pytest.mark.ubuntu -+@pytest.mark.user_data(AUTHORIZED_KEYS2_USERDATA) -+def test_authorized_keys2(client: IntegrationInstance): -+ expected_keys = [ -+ ('test_user1', '/home/test_user1/.ssh/authorized_keys2', -+ TEST_USER1_KEYS), -+ ('test_user2', '/home/test_user2/.ssh/authorized_keys2', -+ TEST_USER2_KEYS), -+ ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2', -+ TEST_DEFAULT_KEYS), -+ ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS), -+ ] -+ common_verify(client, expected_keys) -+ -+ -+NESTED_KEYS_USERDATA = _USERDATA.format(bootcmd=( -+ "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile " -+ "/etc/ssh/authorized_keys %h/foo/bar/ssh/keys;' " -+ "/etc/ssh/sshd_config")) -+ -+ -+@pytest.mark.ubuntu -+@pytest.mark.user_data(NESTED_KEYS_USERDATA) -+def test_nested_keys(client: IntegrationInstance): -+ expected_keys = [ -+ ('test_user1', '/home/test_user1/foo/bar/ssh/keys', -+ TEST_USER1_KEYS), -+ ('test_user2', '/home/test_user2/foo/bar/ssh/keys', -+ TEST_USER2_KEYS), -+ ('ubuntu', '/home/ubuntu/foo/bar/ssh/keys', -+ TEST_DEFAULT_KEYS), -+ ('root', '/root/foo/bar/ssh/keys', TEST_DEFAULT_KEYS), -+ ] -+ common_verify(client, expected_keys) -+ -+ -+EXTERNAL_KEYS_USERDATA = _USERDATA.format(bootcmd=( -+ "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile " -+ "/etc/ssh/authorized_keys /etc/ssh/authorized_keys/%u/keys;' " -+ "/etc/ssh/sshd_config")) -+ -+ -+@pytest.mark.ubuntu -+@pytest.mark.user_data(EXTERNAL_KEYS_USERDATA) -+def test_external_keys(client: IntegrationInstance): -+ expected_keys = [ -+ ('test_user1', '/etc/ssh/authorized_keys/test_user1/keys', -+ TEST_USER1_KEYS), -+ ('test_user2', '/etc/ssh/authorized_keys/test_user2/keys', -+ TEST_USER2_KEYS), -+ ('ubuntu', '/etc/ssh/authorized_keys/ubuntu/keys', -+ TEST_DEFAULT_KEYS), -+ ('root', '/etc/ssh/authorized_keys/root/keys', TEST_DEFAULT_KEYS), -+ ] -+ common_verify(client, expected_keys) --- -2.27.0 - diff --git a/SOURCES/ci-Remove-rhel-specific-files.patch b/SOURCES/ci-Remove-rhel-specific-files.patch new file mode 100644 index 0000000..6765543 --- /dev/null +++ b/SOURCES/ci-Remove-rhel-specific-files.patch @@ -0,0 +1,373 @@ +From d43f0d93386f123892451d923c2b3c6fe7130c39 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Thu, 19 May 2022 11:38:22 +0200 +Subject: [PATCH 4/4] Remove rhel specific files + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 65: Align rhel custom files with upstream (#1431) +RH-Commit: [2/2] 5e31f0bcb500682e7746ccbd2e628c2ef339d6c6 +RH-Bugzilla: 2082071 +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> + +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 <eesposit@redhat.com> +--- + redhat/cloud-init.spec.template | 21 ++------ + 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 cbee197a..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', '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.35.3 + 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..e4e3594 --- /dev/null +++ b/SOURCES/ci-Revert-Add-native-NetworkManager-support-1224.patch @@ -0,0 +1,2266 @@ +From f1836e78d20ef34b05b6aba002fc10a97eceb454 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Mon, 8 Aug 2022 10:08:50 +0200 +Subject: [PATCH 1/2] Revert "Add native NetworkManager support (#1224)" + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 81: Revert "Use Network-Manager and Netplan as default renderers for RHEL and Fedora (#1465)" +RH-Commit: [1/2] 5b3e51502a89c2dcfbc97dc08a86b792454fedd3 +RH-Bugzilla: 2107464 2110066 2117526 2104393 2098624 +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> + +NM is not yet stable, so we don't want to support it for now. +This reverts commit 0d93e53fd05c44b62e3456b7580c9de8135e6b5a. + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + 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 <lkundrak@v3.sk> +-# 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<n> or route<n>, ensuring +- the appropriate value gets used for <n>. +- """ +- +- 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<n> property. +- """ +- +- value = subnet["address"] + "/" + str(subnet["prefix"]) +- self._add_numbered(family, "address", value) +- +- def _add_route(self, family, route): +- """ +- Adds a ipv[46].route<n> 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 c3b0c795..362e8d19 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.27.0 + 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..6532fab --- /dev/null +++ b/SOURCES/ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch @@ -0,0 +1,75 @@ +From 02e7b89c157f8c3243f0d91cf5652cf27db44b72 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Mon, 8 Aug 2022 10:10:26 +0200 +Subject: [PATCH 2/2] Revert "Use Network-Manager and Netplan as default + renderers for RHEL and Fedora (#1465)" + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 81: Revert "Use Network-Manager and Netplan as default renderers for RHEL and Fedora (#1465)" +RH-Commit: [2/2] 746b2e33356376e250b799261031676174e8ccc9 +RH-Bugzilla: 2107464 2110066 2117526 2104393 2098624 +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> + +As NM is reverted, remove also documentation and any trace of it. +This reverts commit 13ded463a6a0b1b0bf0dffc0a997f006dd25c4f3. + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + 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 <https://networkmanager.dev>`_ 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.27.0 + diff --git a/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch b/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch deleted file mode 100644 index c47788f..0000000 --- a/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch +++ /dev/null @@ -1,47 +0,0 @@ -From 0eeec94882779de76c08b1a7faf862e22f21f242 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Fri, 14 Jan 2022 16:42:46 +0100 -Subject: [PATCH 5/6] Revert unnecesary lcase in ds-identify (#978) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 44: Datasource for VMware -RH-Commit: [5/6] f7385c15cf17a9c4a2fa15b29afd1b8a96b24d1e -RH-Bugzilla: 2026587 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -commit f516a7d37c1654addc02485e681b4358d7e7c0db -Author: Andrew Kutz <101085+akutz@users.noreply.github.com> -Date: Fri Aug 13 14:30:55 2021 -0500 - - Revert unnecesary lcase in ds-identify (#978) - - This patch reverts an unnecessary lcase optimization in the - ds-identify script. SystemD documents the values produced by - the systemd-detect-virt command are lower case, and the mapping - table used by the FreeBSD check is also lower-case. - - The optimization added two new forked processes, needlessly - causing overhead. - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - tools/ds-identify | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/tools/ds-identify b/tools/ds-identify -index 0e12298f..7b782462 100755 ---- a/tools/ds-identify -+++ b/tools/ds-identify -@@ -449,7 +449,7 @@ detect_virt() { - read_virt() { - cached "$DI_VIRT" && return 0 - detect_virt -- DI_VIRT="$(echo "${_RET}" | tr '[:upper:]' '[:lower:]')" -+ DI_VIRT="${_RET}" - } - - is_container() { --- -2.27.0 - diff --git a/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch deleted file mode 100644 index e46b52b..0000000 --- a/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch +++ /dev/null @@ -1,1385 +0,0 @@ -From 3b68aff3b7b1dc567ef6721a269c2d4e054b729f Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Mon, 9 Aug 2021 23:41:44 +0200 -Subject: [PATCH] Stop copying ssh system keys and check folder permissions - (#956) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 28: Stop copying ssh system keys and check folder permissions (#956) -RH-Commit: [1/1] 7cada613be82f2f525ee56b86ef9f71edf40d2ef (eesposit/cloud-init) -RH-Bugzilla: 1862967 -RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -TESTED: By me and QA -BREW: 38818284 - -This is a continuation of previous MR 25 and upstream PR #937. -There were still issues when using non-standard file paths like -/etc/ssh/userkeys/%u or /etc/ssh/authorized_keys, and the choice -of storing the keys of all authorized_keys files into a single -one was not ideal. This fix modifies cloudinit to support -all different cases of authorized_keys file locations, and -picks a user-specific file where to copy the new keys that -complies with ssh permissions. - -commit 00dbaf1e9ab0e59d81662f0f3561897bef499a3f -Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Mon Aug 9 16:49:56 2021 +0200 - - Stop copying ssh system keys and check folder permissions (#956) - - In /etc/ssh/sshd_config, it is possible to define a custom - authorized_keys file that will contain the keys allowed to access the - machine via the AuthorizedKeysFile option. Cloudinit is able to add - user-specific keys to the existing ones, but we need to be careful on - which of the authorized_keys files listed to pick. - Chosing a file that is shared by all user will cause security - issues, because the owner of that key can then access also other users. - - We therefore pick an authorized_keys file only if it satisfies the - following conditions: - 1. it is not a "global" file, ie it must be defined in - AuthorizedKeysFile with %u, %h or be in /home/<user>. This avoids - security issues. - 2. it must comply with ssh permission requirements, otherwise the ssh - agent won't use that file. - - If it doesn't meet either of those conditions, write to - ~/.ssh/authorized_keys - - We also need to consider the case when the chosen authorized_keys file - does not exist. In this case, the existing behavior of cloud-init is - to create the new file. We therefore need to be sure that the file - complies with ssh permissions too, by setting: - - the actual file to permission 600, and owned by the user - - the directories in the path that do not exist must be root owned and - with permission 755. - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/ssh_util.py | 133 ++++- - cloudinit/util.py | 51 +- - tests/unittests/test_sshutil.py | 952 +++++++++++++++++++++++++------- - 3 files changed, 920 insertions(+), 216 deletions(-) - -diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py -index 89057262..b8a3c8f7 100644 ---- a/cloudinit/ssh_util.py -+++ b/cloudinit/ssh_util.py -@@ -249,6 +249,113 @@ def render_authorizedkeysfile_paths(value, homedir, username): - return rendered - - -+# Inspired from safe_path() in openssh source code (misc.c). -+def check_permissions(username, current_path, full_path, is_file, strictmodes): -+ """Check if the file/folder in @current_path has the right permissions. -+ -+ We need to check that: -+ 1. If StrictMode is enabled, the owner is either root or the user -+ 2. the user can access the file/folder, otherwise ssh won't use it -+ 3. If StrictMode is enabled, no write permission is given to group -+ and world users (022) -+ """ -+ -+ # group/world can only execute the folder (access) -+ minimal_permissions = 0o711 -+ if is_file: -+ # group/world can only read the file -+ minimal_permissions = 0o644 -+ -+ # 1. owner must be either root or the user itself -+ owner = util.get_owner(current_path) -+ if strictmodes and owner != username and owner != "root": -+ LOG.debug("Path %s in %s must be own by user %s or" -+ " by root, but instead is own by %s. Ignoring key.", -+ current_path, full_path, username, owner) -+ return False -+ -+ parent_permission = util.get_permissions(current_path) -+ # 2. the user can access the file/folder, otherwise ssh won't use it -+ if owner == username: -+ # need only the owner permissions -+ minimal_permissions &= 0o700 -+ else: -+ group_owner = util.get_group(current_path) -+ user_groups = util.get_user_groups(username) -+ -+ if group_owner in user_groups: -+ # need only the group permissions -+ minimal_permissions &= 0o070 -+ else: -+ # need only the world permissions -+ minimal_permissions &= 0o007 -+ -+ if parent_permission & minimal_permissions == 0: -+ LOG.debug("Path %s in %s must be accessible by user %s," -+ " check its permissions", -+ current_path, full_path, username) -+ return False -+ -+ # 3. no write permission (w) is given to group and world users (022) -+ # Group and world user can still have +rx. -+ if strictmodes and parent_permission & 0o022 != 0: -+ LOG.debug("Path %s in %s must not give write" -+ "permission to group or world users. Ignoring key.", -+ current_path, full_path) -+ return False -+ -+ return True -+ -+ -+def check_create_path(username, filename, strictmodes): -+ user_pwent = users_ssh_info(username)[1] -+ root_pwent = users_ssh_info("root")[1] -+ try: -+ # check the directories first -+ directories = filename.split("/")[1:-1] -+ -+ # scan in order, from root to file name -+ parent_folder = "" -+ # this is to comply also with unit tests, and -+ # strange home directories -+ home_folder = os.path.dirname(user_pwent.pw_dir) -+ for directory in directories: -+ parent_folder += "/" + directory -+ if home_folder.startswith(parent_folder): -+ continue -+ -+ if not os.path.isdir(parent_folder): -+ # directory does not exist, and permission so far are good: -+ # create the directory, and make it accessible by everyone -+ # but owned by root, as it might be used by many users. -+ with util.SeLinuxGuard(parent_folder): -+ os.makedirs(parent_folder, mode=0o755, exist_ok=True) -+ util.chownbyid(parent_folder, root_pwent.pw_uid, -+ root_pwent.pw_gid) -+ -+ permissions = check_permissions(username, parent_folder, -+ filename, False, strictmodes) -+ if not permissions: -+ return False -+ -+ # check the file -+ if not os.path.exists(filename): -+ # if file does not exist: we need to create it, since the -+ # folders at this point exist and have right permissions -+ util.write_file(filename, '', mode=0o600, ensure_dir_exists=True) -+ util.chownbyid(filename, user_pwent.pw_uid, user_pwent.pw_gid) -+ -+ permissions = check_permissions(username, filename, -+ filename, True, strictmodes) -+ if not permissions: -+ return False -+ except (IOError, OSError) as e: -+ util.logexc(LOG, str(e)) -+ return False -+ -+ return True -+ -+ - def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - (ssh_dir, pw_ent) = users_ssh_info(username) - default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys') -@@ -259,6 +366,7 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - ssh_cfg = parse_ssh_config_map(sshd_cfg_file) - key_paths = ssh_cfg.get("authorizedkeysfile", - "%h/.ssh/authorized_keys") -+ strictmodes = ssh_cfg.get("strictmodes", "yes") - auth_key_fns = render_authorizedkeysfile_paths( - key_paths, pw_ent.pw_dir, username) - -@@ -269,31 +377,31 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - "config from %r, using 'AuthorizedKeysFile' file " - "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) - -- # check if one of the keys is the user's one -+ # check if one of the keys is the user's one and has the right permissions - for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns): - if any([ - '%u' in key_path, - '%h' in key_path, - auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir)) - ]): -- user_authorizedkeys_file = auth_key_fn -+ permissions_ok = check_create_path(username, auth_key_fn, -+ strictmodes == "yes") -+ if permissions_ok: -+ user_authorizedkeys_file = auth_key_fn -+ break - - if user_authorizedkeys_file != default_authorizedkeys_file: - LOG.debug( - "AuthorizedKeysFile has an user-specific authorized_keys, " - "using %s", user_authorizedkeys_file) - -- # always store all the keys in the user's private file -- return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) -+ return ( -+ user_authorizedkeys_file, -+ parse_authorized_keys([user_authorizedkeys_file]) -+ ) - - - def setup_user_keys(keys, username, options=None): -- # Make sure the users .ssh dir is setup accordingly -- (ssh_dir, pwent) = users_ssh_info(username) -- if not os.path.isdir(ssh_dir): -- util.ensure_dir(ssh_dir, mode=0o700) -- util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid) -- - # Turn the 'update' keys given into actual entries - parser = AuthKeyLineParser() - key_entries = [] -@@ -302,11 +410,10 @@ def setup_user_keys(keys, username, options=None): - - # Extract the old and make the new - (auth_key_fn, auth_key_entries) = extract_authorized_keys(username) -+ ssh_dir = os.path.dirname(auth_key_fn) - with util.SeLinuxGuard(ssh_dir, recursive=True): - content = update_authorized_keys(auth_key_entries, key_entries) -- util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700) -- util.write_file(auth_key_fn, content, mode=0o600) -- util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid) -+ util.write_file(auth_key_fn, content, preserve_mode=True) - - - class SshdConfigLine(object): -diff --git a/cloudinit/util.py b/cloudinit/util.py -index 4e0a72db..343976ad 100644 ---- a/cloudinit/util.py -+++ b/cloudinit/util.py -@@ -35,6 +35,7 @@ from base64 import b64decode, b64encode - from errno import ENOENT - from functools import lru_cache - from urllib import parse -+from typing import List - - from cloudinit import importer - from cloudinit import log as logging -@@ -1830,6 +1831,53 @@ def chmod(path, mode): - os.chmod(path, real_mode) - - -+def get_permissions(path: str) -> int: -+ """ -+ Returns the octal permissions of the file/folder pointed by the path, -+ encoded as an int. -+ -+ @param path: The full path of the file/folder. -+ """ -+ -+ return stat.S_IMODE(os.stat(path).st_mode) -+ -+ -+def get_owner(path: str) -> str: -+ """ -+ Returns the owner of the file/folder pointed by the path. -+ -+ @param path: The full path of the file/folder. -+ """ -+ st = os.stat(path) -+ return pwd.getpwuid(st.st_uid).pw_name -+ -+ -+def get_group(path: str) -> str: -+ """ -+ Returns the group of the file/folder pointed by the path. -+ -+ @param path: The full path of the file/folder. -+ """ -+ st = os.stat(path) -+ return grp.getgrgid(st.st_gid).gr_name -+ -+ -+def get_user_groups(username: str) -> List[str]: -+ """ -+ Returns a list of all groups to which the user belongs -+ -+ @param username: the user we want to check -+ """ -+ groups = [] -+ for group in grp.getgrall(): -+ if username in group.gr_mem: -+ groups.append(group.gr_name) -+ -+ gid = pwd.getpwnam(username).pw_gid -+ groups.append(grp.getgrgid(gid).gr_name) -+ return groups -+ -+ - def write_file( - filename, - content, -@@ -1856,8 +1904,7 @@ def write_file( - - if preserve_mode: - try: -- file_stat = os.stat(filename) -- mode = stat.S_IMODE(file_stat.st_mode) -+ mode = get_permissions(filename) - except OSError: - pass - -diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py -index bcb8044f..a66788bf 100644 ---- a/tests/unittests/test_sshutil.py -+++ b/tests/unittests/test_sshutil.py -@@ -1,6 +1,9 @@ - # This file is part of cloud-init. See LICENSE file for license information. - -+import os -+ - from collections import namedtuple -+from functools import partial - from unittest.mock import patch - - from cloudinit import ssh_util -@@ -8,13 +11,48 @@ from cloudinit.tests import helpers as test_helpers - from cloudinit import util - - # https://stackoverflow.com/questions/11351032/ --FakePwEnt = namedtuple( -- 'FakePwEnt', -- ['pw_dir', 'pw_gecos', 'pw_name', 'pw_passwd', 'pw_shell', 'pwd_uid']) -+FakePwEnt = namedtuple('FakePwEnt', [ -+ 'pw_name', -+ 'pw_passwd', -+ 'pw_uid', -+ 'pw_gid', -+ 'pw_gecos', -+ 'pw_dir', -+ 'pw_shell', -+]) - FakePwEnt.__new__.__defaults__ = tuple( - "UNSET_%s" % n for n in FakePwEnt._fields) - - -+def mock_get_owner(updated_permissions, value): -+ try: -+ return updated_permissions[value][0] -+ except ValueError: -+ return util.get_owner(value) -+ -+ -+def mock_get_group(updated_permissions, value): -+ try: -+ return updated_permissions[value][1] -+ except ValueError: -+ return util.get_group(value) -+ -+ -+def mock_get_user_groups(username): -+ return username -+ -+ -+def mock_get_permissions(updated_permissions, value): -+ try: -+ return updated_permissions[value][2] -+ except ValueError: -+ return util.get_permissions(value) -+ -+ -+def mock_getpwnam(users, username): -+ return users[username] -+ -+ - # Do not use these public keys, most of them are fetched from - # the testdata for OpenSSH, and their private keys are available - # https://github.com/openssh/openssh-portable/tree/master/regress/unittests/sshkey/testdata -@@ -552,12 +590,30 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase): - ssh_util.render_authorizedkeysfile_paths( - "/opt/%u/keys", "/home/bobby", "bobby")) - -+ def test_user_file(self): -+ self.assertEqual( -+ ["/opt/bobby"], -+ ssh_util.render_authorizedkeysfile_paths( -+ "/opt/%u", "/home/bobby", "bobby")) -+ -+ def test_user_file2(self): -+ self.assertEqual( -+ ["/opt/bobby/bobby"], -+ ssh_util.render_authorizedkeysfile_paths( -+ "/opt/%u/%u", "/home/bobby", "bobby")) -+ - def test_multiple(self): - self.assertEqual( - ["/keys/path1", "/keys/path2"], - ssh_util.render_authorizedkeysfile_paths( - "/keys/path1 /keys/path2", "/home/bobby", "bobby")) - -+ def test_multiple2(self): -+ self.assertEqual( -+ ["/keys/path1", "/keys/bobby"], -+ ssh_util.render_authorizedkeysfile_paths( -+ "/keys/path1 /keys/%u", "/home/bobby", "bobby")) -+ - def test_relative(self): - self.assertEqual( - ["/home/bobby/.secret/keys"], -@@ -581,269 +637,763 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase): - - class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - -- @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_order1(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -- m_getpwnam.return_value = fpw -- user_ssh_folder = "%s/.ssh" % fpw.pw_dir -- -- # /tmp/home2/bobby/.ssh/authorized_keys = rsa -- authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) -- util.write_file(authorized_keys, VALID_CONTENT['rsa']) -- -- # /tmp/home2/bobby/.ssh/user_keys = dsa -- user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) -- util.write_file(user_keys, VALID_CONTENT['dsa']) -- -- # /tmp/sshd_config -+ def create_fake_users(self, names, mock_permissions, -+ m_get_group, m_get_owner, m_get_permissions, -+ m_getpwnam, users): -+ homes = [] -+ -+ root = '/tmp/root' -+ fpw = FakePwEnt(pw_name="root", pw_dir=root) -+ users["root"] = fpw -+ -+ for name in names: -+ home = '/tmp/home/' + name -+ fpw = FakePwEnt(pw_name=name, pw_dir=home) -+ users[name] = fpw -+ homes.append(home) -+ -+ m_get_permissions.side_effect = partial( -+ mock_get_permissions, mock_permissions) -+ m_get_owner.side_effect = partial(mock_get_owner, mock_permissions) -+ m_get_group.side_effect = partial(mock_get_group, mock_permissions) -+ m_getpwnam.side_effect = partial(mock_getpwnam, users) -+ return homes -+ -+ def create_user_authorized_file(self, home, filename, content_key, keys): -+ user_ssh_folder = "%s/.ssh" % home -+ # /tmp/home/<user>/.ssh/authorized_keys = content_key -+ authorized_keys = self.tmp_path(filename, dir=user_ssh_folder) -+ util.write_file(authorized_keys, VALID_CONTENT[content_key]) -+ keys[authorized_keys] = content_key -+ return authorized_keys -+ -+ def create_global_authorized_file(self, filename, content_key, keys): -+ authorized_keys = self.tmp_path(filename, dir='/tmp') -+ util.write_file(authorized_keys, VALID_CONTENT[content_key]) -+ keys[authorized_keys] = content_key -+ return authorized_keys -+ -+ def create_sshd_config(self, authorized_keys_files): - sshd_config = self.tmp_path('sshd_config', dir="/tmp") - util.write_file( - sshd_config, -- "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) -+ "AuthorizedKeysFile " + authorized_keys_files - ) -+ return sshd_config - -+ def execute_and_check(self, user, sshd_config, solution, keys, -+ delete_keys=True): - (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -+ user, sshd_config) - content = ssh_util.update_authorized_keys(auth_key_entries, []) - -- self.assertEqual(user_keys, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -+ self.assertEqual(auth_key_fn, solution) -+ for path, key in keys.items(): -+ if path == solution: -+ self.assertTrue(VALID_CONTENT[key] in content) -+ else: -+ self.assertFalse(VALID_CONTENT[key] in content) -+ -+ if delete_keys and os.path.isdir("/tmp/home/"): -+ util.delete_dir_contents("/tmp/home/") - - @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_order2(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') -- m_getpwnam.return_value = fpw -- user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_single_user_two_local_files( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ user_bobby = 'bobby' -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ } -+ -+ homes = self.create_fake_users( -+ [user_bobby], mock_permissions, m_get_group, m_get_owner, -+ m_get_permissions, m_getpwnam, users -+ ) -+ home = homes[0] - -- # /tmp/home/suzie/.ssh/authorized_keys = rsa -- authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) -- util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home, 'authorized_keys', 'rsa', keys -+ ) - -- # /tmp/home/suzie/.ssh/user_keys = dsa -- user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) -- util.write_file(user_keys, VALID_CONTENT['dsa']) -+ # /tmp/home/bobby/.ssh/user_keys = dsa -+ user_keys = self.create_user_authorized_file( -+ home, 'user_keys', 'dsa', keys -+ ) - - # /tmp/sshd_config -- sshd_config = self.tmp_path('sshd_config', dir="/tmp") -- util.write_file( -- sshd_config, -- "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys) -+ options = "%s %s" % (authorized_keys, user_keys) -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_single_user_two_local_files_inverted( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ user_bobby = 'bobby' -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ } -+ -+ homes = self.create_fake_users( -+ [user_bobby], mock_permissions, m_get_group, m_get_owner, -+ m_get_permissions, m_getpwnam, users - ) -+ home = homes[0] - -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home, 'authorized_keys', 'rsa', keys -+ ) - -- self.assertEqual(authorized_keys, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -+ # /tmp/home/bobby/.ssh/user_keys = dsa -+ user_keys = self.create_user_authorized_file( -+ home, 'user_keys', 'dsa', keys -+ ) - -- @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -- m_getpwnam.return_value = fpw -- user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ # /tmp/sshd_config -+ options = "%s %s" % (user_keys, authorized_keys) -+ sshd_config = self.create_sshd_config(options) - -- # /tmp/home2/bobby/.ssh/authorized_keys = rsa -- authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) -- util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ self.execute_and_check(user_bobby, sshd_config, user_keys, keys) - -- # /tmp/home2/bobby/.ssh/user_keys = dsa -- user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) -- util.write_file(user_keys, VALID_CONTENT['dsa']) -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_single_user_local_global_files( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ user_bobby = 'bobby' -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ } -+ -+ homes = self.create_fake_users( -+ [user_bobby], mock_permissions, m_get_group, m_get_owner, -+ m_get_permissions, m_getpwnam, users -+ ) -+ home = homes[0] - -- # /tmp/etc/ssh/authorized_keys = ecdsa -- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', -- dir="/tmp") -- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home, 'authorized_keys', 'rsa', keys -+ ) - -- # /tmp/sshd_config -- sshd_config = self.tmp_path('sshd_config', dir="/tmp") -- util.write_file( -- sshd_config, -- "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, -- user_keys, authorized_keys) -+ # /tmp/home/bobby/.ssh/user_keys = dsa -+ user_keys = self.create_user_authorized_file( -+ home, 'user_keys', 'dsa', keys - ) - -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ authorized_keys_global = self.create_global_authorized_file( -+ 'etc/ssh/authorized_keys', 'ecdsa', keys -+ ) - -- self.assertEqual(authorized_keys, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -- self.assertTrue(VALID_CONTENT['ecdsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -+ options = "%s %s %s" % (authorized_keys_global, user_keys, -+ authorized_keys) -+ sshd_config = self.create_sshd_config(options) - -- @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -- m_getpwnam.return_value = fpw -- user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ self.execute_and_check(user_bobby, sshd_config, user_keys, keys) - -- # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa -- authorized_keys = self.tmp_path('authorized_keys2', -- dir=user_ssh_folder) -- util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_single_user_local_global_files_inverted( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ user_bobby = 'bobby' -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600), -+ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), -+ } -+ -+ homes = self.create_fake_users( -+ [user_bobby], mock_permissions, m_get_group, m_get_owner, -+ m_get_permissions, m_getpwnam, users -+ ) -+ home = homes[0] - -- # /tmp/home2/bobby/.ssh/user_keys3 = dsa -- user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) -- util.write_file(user_keys, VALID_CONTENT['dsa']) -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home, 'authorized_keys2', 'rsa', keys -+ ) - -- # /tmp/etc/ssh/authorized_keys = ecdsa -- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', -- dir="/tmp") -- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ # /tmp/home/bobby/.ssh/user_keys = dsa -+ user_keys = self.create_user_authorized_file( -+ home, 'user_keys3', 'dsa', keys -+ ) - -- # /tmp/sshd_config -- sshd_config = self.tmp_path('sshd_config', dir="/tmp") -- util.write_file( -- sshd_config, -- "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, -- authorized_keys, user_keys) -+ authorized_keys_global = self.create_global_authorized_file( -+ 'etc/ssh/authorized_keys', 'ecdsa', keys - ) - -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ options = "%s %s %s" % (authorized_keys_global, authorized_keys, -+ user_keys) -+ sshd_config = self.create_sshd_config(options) - -- self.assertEqual(user_keys, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -- self.assertTrue(VALID_CONTENT['ecdsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -+ self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys) - - @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_global(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -- m_getpwnam.return_value = fpw -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_single_user_global_file( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ user_bobby = 'bobby' -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ } -+ -+ homes = self.create_fake_users( -+ [user_bobby], mock_permissions, m_get_group, m_get_owner, -+ m_get_permissions, m_getpwnam, users -+ ) -+ home = homes[0] - - # /tmp/etc/ssh/authorized_keys = rsa -- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', -- dir="/tmp") -- util.write_file(authorized_keys_global, VALID_CONTENT['rsa']) -+ authorized_keys_global = self.create_global_authorized_file( -+ 'etc/ssh/authorized_keys', 'rsa', keys -+ ) - -- # /tmp/sshd_config -- sshd_config = self.tmp_path('sshd_config') -- util.write_file( -- sshd_config, -- "AuthorizedKeysFile %s" % (authorized_keys_global) -+ options = "%s" % authorized_keys_global -+ sshd_config = self.create_sshd_config(options) -+ -+ default = "%s/.ssh/authorized_keys" % home -+ self.execute_and_check(user_bobby, sshd_config, default, keys) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_local_file_standard( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_suzie = 'suzie' -+ homes = self.create_fake_users( -+ [user_bobby, user_suzie], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users - ) -+ home_bobby = homes[0] -+ home_suzie = homes[1] - -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home_bobby, 'authorized_keys', 'rsa', keys -+ ) - -- self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -+ # /tmp/home/suzie/.ssh/authorized_keys = rsa -+ authorized_keys2 = self.create_user_authorized_file( -+ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys -+ ) -+ -+ options = ".ssh/authorized_keys" -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) - - @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -- m_getpwnam.return_value = fpw -- user_ssh_folder = "%s/.ssh" % fpw.pw_dir -- # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa -- authorized_keys = self.tmp_path('authorized_keys2', -- dir=user_ssh_folder) -- util.write_file(authorized_keys, VALID_CONTENT['rsa']) -- # /tmp/home2/bobby/.ssh/user_keys3 = dsa -- user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) -- util.write_file(user_keys, VALID_CONTENT['dsa']) -- -- fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') -- user_ssh_folder = "%s/.ssh" % fpw2.pw_dir -- # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com -- authorized_keys2 = self.tmp_path('authorized_keys2', -- dir=user_ssh_folder) -- util.write_file(authorized_keys2, -- VALID_CONTENT['ssh-xmss@openssh.com']) -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_local_file_custom( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), -+ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_suzie = 'suzie' -+ homes = self.create_fake_users( -+ [user_bobby, user_suzie], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ home_bobby = homes[0] -+ home_suzie = homes[1] - -- # /tmp/etc/ssh/authorized_keys = ecdsa -- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', -- dir="/tmp") -- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ # /tmp/home/bobby/.ssh/authorized_keys2 = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home_bobby, 'authorized_keys2', 'rsa', keys -+ ) - -- # /tmp/sshd_config -- sshd_config = self.tmp_path('sshd_config', dir="/tmp") -- util.write_file( -- sshd_config, -- "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" % -- (authorized_keys_global, user_keys) -+ # /tmp/home/suzie/.ssh/authorized_keys2 = rsa -+ authorized_keys2 = self.create_user_authorized_file( -+ home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys - ) - -- # process first user -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ options = ".ssh/authorized_keys2" -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) - -- self.assertEqual(user_keys, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -- self.assertTrue(VALID_CONTENT['ecdsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -- self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content) -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_local_global_files( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), -+ '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600), -+ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600), -+ '/tmp/home/suzie/.ssh/user_keys3': ('suzie', 'suzie', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_suzie = 'suzie' -+ homes = self.create_fake_users( -+ [user_bobby, user_suzie], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ home_bobby = homes[0] -+ home_suzie = homes[1] - -- m_getpwnam.return_value = fpw2 -- # process second user -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw2.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ # /tmp/home/bobby/.ssh/authorized_keys2 = rsa -+ self.create_user_authorized_file( -+ home_bobby, 'authorized_keys2', 'rsa', keys -+ ) -+ # /tmp/home/bobby/.ssh/user_keys3 = dsa -+ user_keys = self.create_user_authorized_file( -+ home_bobby, 'user_keys3', 'dsa', keys -+ ) -+ -+ # /tmp/home/suzie/.ssh/authorized_keys2 = rsa -+ authorized_keys2 = self.create_user_authorized_file( -+ home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys -+ ) -+ -+ # /tmp/etc/ssh/authorized_keys = ecdsa -+ authorized_keys_global = self.create_global_authorized_file( -+ 'etc/ssh/authorized_keys2', 'ecdsa', keys -+ ) -+ -+ options = "%s %s %%h/.ssh/authorized_keys2" % \ -+ (authorized_keys_global, user_keys) -+ sshd_config = self.create_sshd_config(options) - -- self.assertEqual(authorized_keys2, auth_key_fn) -- self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content) -- self.assertTrue(VALID_CONTENT['ecdsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -- self.assertFalse(VALID_CONTENT['rsa'] in content) -+ self.execute_and_check( -+ user_bobby, sshd_config, user_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) - -+ @patch("cloudinit.util.get_user_groups") - @patch("cloudinit.ssh_util.pwd.getpwnam") -- def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby') -- m_getpwnam.return_value = fpw -- user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_local_global_files_badguy( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, -+ m_get_user_groups -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600), -+ '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600), -+ '/tmp/home/badguy': ('root', 'root', 0o755), -+ '/tmp/home/badguy/home': ('root', 'root', 0o755), -+ '/tmp/home/badguy/home/bobby': ('root', 'root', 0o655), -+ } -+ -+ user_bobby = 'bobby' -+ user_badguy = 'badguy' -+ home_bobby, *_ = self.create_fake_users( -+ [user_bobby, user_badguy], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ m_get_user_groups.side_effect = mock_get_user_groups -+ - # /tmp/home/bobby/.ssh/authorized_keys2 = rsa -- authorized_keys = self.tmp_path('authorized_keys2', -- dir=user_ssh_folder) -- util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ authorized_keys = self.create_user_authorized_file( -+ home_bobby, 'authorized_keys2', 'rsa', keys -+ ) - # /tmp/home/bobby/.ssh/user_keys3 = dsa -- user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) -- util.write_file(user_keys, VALID_CONTENT['dsa']) -+ user_keys = self.create_user_authorized_file( -+ home_bobby, 'user_keys3', 'dsa', keys -+ ) - -- fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy') -- user_ssh_folder = "%s/.ssh" % fpw2.pw_dir - # /tmp/home/badguy/home/bobby = "" - authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy") -+ util.write_file(authorized_keys2, '') - - # /tmp/etc/ssh/authorized_keys = ecdsa -- authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', -- dir="/tmp") -- util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ authorized_keys_global = self.create_global_authorized_file( -+ 'etc/ssh/authorized_keys2', 'ecdsa', keys -+ ) - - # /tmp/sshd_config -- sshd_config = self.tmp_path('sshd_config', dir="/tmp") -- util.write_file( -- sshd_config, -- "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" % -- (authorized_keys_global, user_keys, authorized_keys2) -+ options = "%s %%h/.ssh/authorized_keys2 %s %s" % \ -+ (authorized_keys2, authorized_keys_global, user_keys) -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check( -+ user_badguy, sshd_config, authorized_keys2, keys - ) - -- # process first user -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ @patch("cloudinit.util.get_user_groups") -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_unaccessible_file( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, -+ m_get_user_groups -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ -+ '/tmp/etc': ('root', 'root', 0o755), -+ '/tmp/etc/ssh': ('root', 'root', 0o755), -+ '/tmp/etc/ssh/userkeys': ('root', 'root', 0o700), -+ '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600), -+ '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600), -+ -+ '/tmp/home/badguy': ('badguy', 'badguy', 0o700), -+ '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700), -+ '/tmp/home/badguy/.ssh/authorized_keys': -+ ('badguy', 'badguy', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_badguy = 'badguy' -+ homes = self.create_fake_users( -+ [user_bobby, user_badguy], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ m_get_user_groups.side_effect = mock_get_user_groups -+ home_bobby = homes[0] -+ home_badguy = homes[1] - -- self.assertEqual(user_keys, auth_key_fn) -- self.assertTrue(VALID_CONTENT['rsa'] in content) -- self.assertTrue(VALID_CONTENT['ecdsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home_bobby, 'authorized_keys', 'rsa', keys -+ ) -+ # /tmp/etc/ssh/userkeys/bobby = dsa -+ # assume here that we can bypass userkeys, despite permissions -+ self.create_global_authorized_file( -+ 'etc/ssh/userkeys/bobby', 'dsa', keys -+ ) - -- m_getpwnam.return_value = fpw2 -- # process second user -- (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw2.pw_name, sshd_config) -- content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com -+ authorized_keys2 = self.create_user_authorized_file( -+ home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys -+ ) - -- # badguy should not take the key from the other user! -- self.assertEqual(authorized_keys2, auth_key_fn) -- self.assertTrue(VALID_CONTENT['ecdsa'] in content) -- self.assertTrue(VALID_CONTENT['dsa'] in content) -- self.assertFalse(VALID_CONTENT['rsa'] in content) -+ # /tmp/etc/ssh/userkeys/badguy = ecdsa -+ self.create_global_authorized_file( -+ 'etc/ssh/userkeys/badguy', 'ecdsa', keys -+ ) -+ -+ # /tmp/sshd_config -+ options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys" -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check( -+ user_badguy, sshd_config, authorized_keys2, keys -+ ) -+ -+ @patch("cloudinit.util.get_user_groups") -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_accessible_file( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, -+ m_get_user_groups -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ -+ '/tmp/etc': ('root', 'root', 0o755), -+ '/tmp/etc/ssh': ('root', 'root', 0o755), -+ '/tmp/etc/ssh/userkeys': ('root', 'root', 0o755), -+ '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600), -+ '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600), -+ -+ '/tmp/home/badguy': ('badguy', 'badguy', 0o700), -+ '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700), -+ '/tmp/home/badguy/.ssh/authorized_keys': -+ ('badguy', 'badguy', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_badguy = 'badguy' -+ homes = self.create_fake_users( -+ [user_bobby, user_badguy], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ m_get_user_groups.side_effect = mock_get_user_groups -+ home_bobby = homes[0] -+ home_badguy = homes[1] -+ -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ self.create_user_authorized_file( -+ home_bobby, 'authorized_keys', 'rsa', keys -+ ) -+ # /tmp/etc/ssh/userkeys/bobby = dsa -+ # assume here that we can bypass userkeys, despite permissions -+ authorized_keys = self.create_global_authorized_file( -+ 'etc/ssh/userkeys/bobby', 'dsa', keys -+ ) -+ -+ # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com -+ self.create_user_authorized_file( -+ home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys -+ ) -+ -+ # /tmp/etc/ssh/userkeys/badguy = ecdsa -+ authorized_keys2 = self.create_global_authorized_file( -+ 'etc/ssh/userkeys/badguy', 'ecdsa', keys -+ ) -+ -+ # /tmp/sshd_config -+ options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys" -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check( -+ user_badguy, sshd_config, authorized_keys2, keys -+ ) -+ -+ @patch("cloudinit.util.get_user_groups") -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_hardcoded_single_user_file( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, -+ m_get_user_groups -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ -+ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_suzie = 'suzie' -+ homes = self.create_fake_users( -+ [user_bobby, user_suzie], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ home_bobby = homes[0] -+ home_suzie = homes[1] -+ m_get_user_groups.side_effect = mock_get_user_groups -+ -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home_bobby, 'authorized_keys', 'rsa', keys -+ ) -+ -+ # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com -+ self.create_user_authorized_file( -+ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys -+ ) -+ -+ # /tmp/sshd_config -+ options = "%s" % (authorized_keys) -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ default = "%s/.ssh/authorized_keys" % home_suzie -+ self.execute_and_check(user_suzie, sshd_config, default, keys) -+ -+ @patch("cloudinit.util.get_user_groups") -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_hardcoded_single_user_file_inverted( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, -+ m_get_user_groups -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ -+ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_suzie = 'suzie' -+ homes = self.create_fake_users( -+ [user_bobby, user_suzie], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ home_bobby = homes[0] -+ home_suzie = homes[1] -+ m_get_user_groups.side_effect = mock_get_user_groups -+ -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ self.create_user_authorized_file( -+ home_bobby, 'authorized_keys', 'rsa', keys -+ ) -+ -+ # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com -+ authorized_keys2 = self.create_user_authorized_file( -+ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys -+ ) -+ -+ # /tmp/sshd_config -+ options = "%s" % (authorized_keys2) -+ sshd_config = self.create_sshd_config(options) -+ -+ default = "%s/.ssh/authorized_keys" % home_bobby -+ self.execute_and_check( -+ user_bobby, sshd_config, default, keys, delete_keys=False -+ ) -+ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) -+ -+ @patch("cloudinit.util.get_user_groups") -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ @patch("cloudinit.util.get_permissions") -+ @patch("cloudinit.util.get_owner") -+ @patch("cloudinit.util.get_group") -+ def test_two_users_hardcoded_user_files( -+ self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam, -+ m_get_user_groups -+ ): -+ keys = {} -+ users = {} -+ mock_permissions = { -+ '/tmp/home/bobby': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700), -+ '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600), -+ -+ '/tmp/home/suzie': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700), -+ '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600), -+ } -+ -+ user_bobby = 'bobby' -+ user_suzie = 'suzie' -+ homes = self.create_fake_users( -+ [user_bobby, user_suzie], mock_permissions, m_get_group, -+ m_get_owner, m_get_permissions, m_getpwnam, users -+ ) -+ home_bobby = homes[0] -+ home_suzie = homes[1] -+ m_get_user_groups.side_effect = mock_get_user_groups -+ -+ # /tmp/home/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.create_user_authorized_file( -+ home_bobby, 'authorized_keys', 'rsa', keys -+ ) -+ -+ # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com -+ authorized_keys2 = self.create_user_authorized_file( -+ home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys -+ ) -+ -+ # /tmp/etc/ssh/authorized_keys = ecdsa -+ authorized_keys_global = self.create_global_authorized_file( -+ 'etc/ssh/authorized_keys', 'ecdsa', keys -+ ) -+ -+ # /tmp/sshd_config -+ options = "%s %s %s" % \ -+ (authorized_keys_global, authorized_keys, authorized_keys2) -+ sshd_config = self.create_sshd_config(options) -+ -+ self.execute_and_check( -+ user_bobby, sshd_config, authorized_keys, keys, delete_keys=False -+ ) -+ self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys) - - # vi: ts=4 expandtab --- -2.27.0 - 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..6e8e0fb --- /dev/null +++ b/SOURCES/ci-Support-EC2-tags-in-instance-metadata-1309.patch @@ -0,0 +1,164 @@ +From fbec3008305845072a787f46008bbb82d89dec53 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Mon, 30 May 2022 16:46:41 +0200 +Subject: [PATCH] Support EC2 tags in instance metadata (#1309) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 70: Support EC2 tags in instance metadata (#1309) +RH-Commit: [1/1] 2497547016173a4c6e7d3c900f80de390d445c44 +RH-Bugzilla: 2082686 +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> + +commit 40c52ce1f4049449b04f93226721f63af874c5c7 +Author: Eduardo Dobay <edudobay@users.noreply.github.com> +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 <eesposit@redhat.com> +--- + 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 <url>/<version>/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.27.0 + diff --git a/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch b/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch deleted file mode 100644 index 07c44fe..0000000 --- a/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch +++ /dev/null @@ -1,97 +0,0 @@ -From ded01bd47c65636e59dc332d06fb8acb982ec677 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Fri, 14 Jan 2022 16:41:52 +0100 -Subject: [PATCH 4/6] Update dscheck_VMware's rpctool check (#970) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 44: Datasource for VMware -RH-Commit: [4/6] 509f68596f2d8f32027677f756b9d81e6a507ff1 -RH-Bugzilla: 2026587 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -commit 7781dec3306e9467f216cfcb36b7e10a8b38547a -Author: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com> -Date: Fri Aug 13 00:40:39 2021 +0530 - - Update dscheck_VMware's rpctool check (#970) - - This patch updates the dscheck_VMware function's use of "vmware-rpctool". - - When checking to see if a "guestinfo" property is set. - Because a successful exit code can occur even if there is an empty - string returned, it is possible that the VMware datasource will be - loaded as a false-positive. This patch ensures that in addition to - validating the exit code, the emitted output is also examined to ensure - a non-empty value is returned by rpctool before returning "${DS_FOUND}" - from "dscheck_VMware()". - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - tools/ds-identify | 15 +++++++++------ - 1 file changed, 9 insertions(+), 6 deletions(-) - -diff --git a/tools/ds-identify b/tools/ds-identify -index c01eae3d..0e12298f 100755 ---- a/tools/ds-identify -+++ b/tools/ds-identify -@@ -141,6 +141,7 @@ error() { - debug 0 "$@" - stderr "$@" - } -+ - warn() { - set -- "WARN:" "$@" - debug 0 "$@" -@@ -344,7 +345,6 @@ geom_label_status_as() { - return $ret - } - -- - read_fs_info_freebsd() { - local oifs="$IFS" line="" delim="," - local ret=0 labels="" dev="" label="" ftype="" isodevs="" -@@ -404,7 +404,6 @@ cached() { - [ -n "$1" ] && _RET="$1" && return || return 1 - } - -- - detect_virt() { - local virt="${UNAVAILABLE}" r="" out="" - if [ -d /run/systemd ]; then -@@ -450,7 +449,7 @@ detect_virt() { - read_virt() { - cached "$DI_VIRT" && return 0 - detect_virt -- DI_VIRT=${_RET} -+ DI_VIRT="$(echo "${_RET}" | tr '[:upper:]' '[:lower:]')" - } - - is_container() { -@@ -1370,16 +1369,20 @@ vmware_has_rpctool() { - command -v vmware-rpctool >/dev/null 2>&1 - } - -+vmware_rpctool_guestinfo() { -+ vmware-rpctool "info-get guestinfo.${1}" 2>/dev/null | grep "[[:alnum:]]" -+} -+ - vmware_rpctool_guestinfo_metadata() { -- vmware-rpctool "info-get guestinfo.metadata" -+ vmware_rpctool_guestinfo "metadata" - } - - vmware_rpctool_guestinfo_userdata() { -- vmware-rpctool "info-get guestinfo.userdata" -+ vmware_rpctool_guestinfo "userdata" - } - - vmware_rpctool_guestinfo_vendordata() { -- vmware-rpctool "info-get guestinfo.vendordata" -+ vmware_rpctool_guestinfo "vendordata" - } - - dscheck_VMware() { --- -2.27.0 - 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..04d5e1f --- /dev/null +++ b/SOURCES/ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch @@ -0,0 +1,110 @@ +From 13ded463a6a0b1b0bf0dffc0a997f006dd25c4f3 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Thu, 19 May 2022 15:51:27 +0200 +Subject: [PATCH 2/4] Use Network-Manager and Netplan as default renderers for + RHEL and Fedora (#1465) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 57: Add native NetworkManager support (#1224) +RH-Commit: [2/2] f2f977564bea496b0d76c0cef242959d03c2c73e +RH-Bugzilla: 2059872 +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Jon Maloy <jmaloy@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> + +commit 7703aa98b89c8daba207c28a0422268ead10019a +Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +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 <ngompa13@gmail.com> + + Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + 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 <https://networkmanager.dev>`_ 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.35.3 + diff --git a/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch b/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch deleted file mode 100644 index 1ccfec9..0000000 --- a/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch +++ /dev/null @@ -1,470 +0,0 @@ -From 6e79106a09a0d142915da1fb48640575bb4bfe08 Mon Sep 17 00:00:00 2001 -From: Anh Vo <anhvo@microsoft.com> -Date: Tue, 13 Apr 2021 17:39:39 -0400 -Subject: [PATCH 3/7] azure: Removing ability to invoke walinuxagent (#799) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 45: Add support for userdata on Azure from IMDS -RH-Commit: [3/7] f5e98665bf2093edeeccfcd95b47df2e44a40536 -RH-Bugzilla: 2023940 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -Invoking walinuxagent from within cloud-init is no longer -supported/necessary ---- - cloudinit/sources/DataSourceAzure.py | 137 ++++-------------- - doc/rtd/topics/datasources/azure.rst | 62 ++------ - tests/unittests/test_datasource/test_azure.py | 97 ------------- - 3 files changed, 35 insertions(+), 261 deletions(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index de1452ce..020b7006 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -381,53 +381,6 @@ class DataSourceAzure(sources.DataSource): - util.logexc(LOG, "handling set_hostname failed") - return False - -- @azure_ds_telemetry_reporter -- def get_metadata_from_agent(self): -- temp_hostname = self.metadata.get('local-hostname') -- agent_cmd = self.ds_cfg['agent_command'] -- LOG.debug("Getting metadata via agent. hostname=%s cmd=%s", -- temp_hostname, agent_cmd) -- -- self.bounce_network_with_azure_hostname() -- -- try: -- invoke_agent(agent_cmd) -- except subp.ProcessExecutionError: -- # claim the datasource even if the command failed -- util.logexc(LOG, "agent command '%s' failed.", -- self.ds_cfg['agent_command']) -- -- ddir = self.ds_cfg['data_dir'] -- -- fp_files = [] -- key_value = None -- for pk in self.cfg.get('_pubkeys', []): -- if pk.get('value', None): -- key_value = pk['value'] -- LOG.debug("SSH authentication: using value from fabric") -- else: -- bname = str(pk['fingerprint'] + ".crt") -- fp_files += [os.path.join(ddir, bname)] -- LOG.debug("SSH authentication: " -- "using fingerprint from fabric") -- -- with events.ReportEventStack( -- name="waiting-for-ssh-public-key", -- description="wait for agents to retrieve SSH keys", -- parent=azure_ds_reporter): -- # wait very long for public SSH keys to arrive -- # https://bugs.launchpad.net/cloud-init/+bug/1717611 -- missing = util.log_time(logfunc=LOG.debug, -- msg="waiting for SSH public key files", -- func=util.wait_for_files, -- args=(fp_files, 900)) -- if len(missing): -- LOG.warning("Did not find files, but going on: %s", missing) -- -- metadata = {} -- metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files) -- return metadata -- - def _get_subplatform(self): - """Return the subplatform metadata source details.""" - if self.seed.startswith('/dev'): -@@ -1354,35 +1307,32 @@ class DataSourceAzure(sources.DataSource): - On failure, returns False. - """ - -- if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN: -- self.bounce_network_with_azure_hostname() -+ self.bounce_network_with_azure_hostname() - -- pubkey_info = None -- try: -- raise KeyError( -- "Not using public SSH keys from IMDS" -- ) -- # pylint:disable=unreachable -- public_keys = self.metadata['imds']['compute']['publicKeys'] -- LOG.debug( -- 'Successfully retrieved %s key(s) from IMDS', -- len(public_keys) -- if public_keys is not None -- else 0 -- ) -- except KeyError: -- LOG.debug( -- 'Unable to retrieve SSH keys from IMDS during ' -- 'negotiation, falling back to OVF' -- ) -- pubkey_info = self.cfg.get('_pubkeys', None) -- -- metadata_func = partial(get_metadata_from_fabric, -- fallback_lease_file=self. -- dhclient_lease_file, -- pubkey_info=pubkey_info) -- else: -- metadata_func = self.get_metadata_from_agent -+ pubkey_info = None -+ try: -+ raise KeyError( -+ "Not using public SSH keys from IMDS" -+ ) -+ # pylint:disable=unreachable -+ public_keys = self.metadata['imds']['compute']['publicKeys'] -+ LOG.debug( -+ 'Successfully retrieved %s key(s) from IMDS', -+ len(public_keys) -+ if public_keys is not None -+ else 0 -+ ) -+ except KeyError: -+ LOG.debug( -+ 'Unable to retrieve SSH keys from IMDS during ' -+ 'negotiation, falling back to OVF' -+ ) -+ pubkey_info = self.cfg.get('_pubkeys', None) -+ -+ metadata_func = partial(get_metadata_from_fabric, -+ fallback_lease_file=self. -+ dhclient_lease_file, -+ pubkey_info=pubkey_info) - - LOG.debug("negotiating with fabric via agent command %s", - self.ds_cfg['agent_command']) -@@ -1617,33 +1567,6 @@ def perform_hostname_bounce(hostname, cfg, prev_hostname): - return True - - --@azure_ds_telemetry_reporter --def crtfile_to_pubkey(fname, data=None): -- pipeline = ('openssl x509 -noout -pubkey < "$0" |' -- 'ssh-keygen -i -m PKCS8 -f /dev/stdin') -- (out, _err) = subp.subp(['sh', '-c', pipeline, fname], -- capture=True, data=data) -- return out.rstrip() -- -- --@azure_ds_telemetry_reporter --def pubkeys_from_crt_files(flist): -- pubkeys = [] -- errors = [] -- for fname in flist: -- try: -- pubkeys.append(crtfile_to_pubkey(fname)) -- except subp.ProcessExecutionError: -- errors.append(fname) -- -- if errors: -- report_diagnostic_event( -- "failed to convert the crt files to pubkey: %s" % errors, -- logger_func=LOG.warning) -- -- return pubkeys -- -- - @azure_ds_telemetry_reporter - def write_files(datadir, files, dirmode=None): - -@@ -1672,16 +1595,6 @@ def write_files(datadir, files, dirmode=None): - util.write_file(filename=fname, content=content, mode=0o600) - - --@azure_ds_telemetry_reporter --def invoke_agent(cmd): -- # this is a function itself to simplify patching it for test -- if cmd: -- LOG.debug("invoking agent: %s", cmd) -- subp.subp(cmd, shell=(not isinstance(cmd, list))) -- else: -- LOG.debug("not invoking agent") -- -- - def find_child(node, filter_func): - ret = [] - if not node.hasChildNodes(): -diff --git a/doc/rtd/topics/datasources/azure.rst b/doc/rtd/topics/datasources/azure.rst -index e04c3a33..ad9f2236 100644 ---- a/doc/rtd/topics/datasources/azure.rst -+++ b/doc/rtd/topics/datasources/azure.rst -@@ -5,28 +5,6 @@ Azure - - This datasource finds metadata and user-data from the Azure cloud platform. - --walinuxagent -------------- --walinuxagent has several functions within images. For cloud-init --specifically, the relevant functionality it performs is to register the --instance with the Azure cloud platform at boot so networking will be --permitted. For more information about the other functionality of --walinuxagent, see `Azure's documentation --<https://github.com/Azure/WALinuxAgent#introduction>`_ for more details. --(Note, however, that only one of walinuxagent's provisioning and cloud-init --should be used to perform instance customisation.) -- --If you are configuring walinuxagent yourself, you will want to ensure that you --have `Provisioning.UseCloudInit --<https://github.com/Azure/WALinuxAgent#provisioningusecloudinit>`_ set to --``y``. -- -- --Builtin Agent --------------- --An alternative to using walinuxagent to register to the Azure cloud platform --is to use the ``__builtin__`` agent command. This section contains more --background on what that code path does, and how to enable it. - - The Azure cloud platform provides initial data to an instance via an attached - CD formatted in UDF. That CD contains a 'ovf-env.xml' file that provides some -@@ -41,16 +19,6 @@ by calling a script in /etc/dhcp/dhclient-exit-hooks or a file in - 'dhclient_hook' of cloud-init itself. This sub-command will write the client - information in json format to /run/cloud-init/dhclient.hook/<interface>.json. - --In order for cloud-init to leverage this method to find the endpoint, the --cloud.cfg file must contain: -- --.. sourcecode:: yaml -- -- datasource: -- Azure: -- set_hostname: False -- agent_command: __builtin__ -- - If those files are not available, the fallback is to check the leases file - for the endpoint server (again option 245). - -@@ -83,9 +51,6 @@ configuration (in ``/etc/cloud/cloud.cfg`` or ``/etc/cloud/cloud.cfg.d/``). - - The settings that may be configured are: - -- * **agent_command**: Either __builtin__ (default) or a command to run to getcw -- metadata. If __builtin__, get metadata from walinuxagent. Otherwise run the -- provided command to obtain metadata. - * **apply_network_config**: Boolean set to True to use network configuration - described by Azure's IMDS endpoint instead of fallback network config of - dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is -@@ -121,7 +86,6 @@ An example configuration with the default values is provided below: - - datasource: - Azure: -- agent_command: __builtin__ - apply_network_config: true - data_dir: /var/lib/waagent - dhclient_lease_file: /var/lib/dhcp/dhclient.eth0.leases -@@ -144,9 +108,7 @@ child of the ``LinuxProvisioningConfigurationSet`` (a sibling to ``UserName``) - If both ``UserData`` and ``CustomData`` are provided behavior is undefined on - which will be selected. - --In the example below, user-data provided is 'this is my userdata', and the --datasource config provided is ``{"agent_command": ["start", "walinuxagent"]}``. --That agent command will take affect as if it were specified in system config. -+In the example below, user-data provided is 'this is my userdata' - - Example: - -@@ -184,20 +146,16 @@ The hostname is provided to the instance in the ovf-env.xml file as - Whatever value the instance provides in its dhcp request will resolve in the - domain returned in the 'search' request. - --The interesting issue is that a generic image will already have a hostname --configured. The ubuntu cloud images have 'ubuntu' as the hostname of the --system, and the initial dhcp request on eth0 is not guaranteed to occur after --the datasource code has been run. So, on first boot, that initial value will --be sent in the dhcp request and *that* value will resolve. -- --In order to make the ``HostName`` provided in the ovf-env.xml resolve, a --dhcp request must be made with the new value. Walinuxagent (in its current --version) handles this by polling the state of hostname and bouncing ('``ifdown --eth0; ifup eth0``' the network interface if it sees that a change has been --made. -+A generic image will already have a hostname configured. The ubuntu -+cloud images have 'ubuntu' as the hostname of the system, and the -+initial dhcp request on eth0 is not guaranteed to occur after the -+datasource code has been run. So, on first boot, that initial value -+will be sent in the dhcp request and *that* value will resolve. - --cloud-init handles this by setting the hostname in the DataSource's 'get_data' --method via '``hostname $HostName``', and then bouncing the interface. This -+In order to make the ``HostName`` provided in the ovf-env.xml resolve, -+a dhcp request must be made with the new value. cloud-init handles -+this by setting the hostname in the DataSource's 'get_data' method via -+'``hostname $HostName``', and then bouncing the interface. This - behavior can be configured or disabled in the datasource config. See - 'Configuration' above. - -diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py -index dedebeb1..320fa857 100644 ---- a/tests/unittests/test_datasource/test_azure.py -+++ b/tests/unittests/test_datasource/test_azure.py -@@ -638,17 +638,10 @@ scbus-1 on xpt0 bus 0 - def dsdevs(): - return data.get('dsdevs', []) - -- def _invoke_agent(cmd): -- data['agent_invoked'] = cmd -- - def _wait_for_files(flist, _maxwait=None, _naplen=None): - data['waited'] = flist - return [] - -- def _pubkeys_from_crt_files(flist): -- data['pubkey_files'] = flist -- return ["pubkey_from: %s" % f for f in flist] -- - if data.get('ovfcontent') is not None: - populate_dir(os.path.join(self.paths.seed_dir, "azure"), - {'ovf-env.xml': data['ovfcontent']}) -@@ -675,8 +668,6 @@ scbus-1 on xpt0 bus 0 - - self.apply_patches([ - (dsaz, 'list_possible_azure_ds_devs', dsdevs), -- (dsaz, 'invoke_agent', _invoke_agent), -- (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files), - (dsaz, 'perform_hostname_bounce', mock.MagicMock()), - (dsaz, 'get_hostname', mock.MagicMock()), - (dsaz, 'set_hostname', mock.MagicMock()), -@@ -765,7 +756,6 @@ scbus-1 on xpt0 bus 0 - ret = dsrc.get_data() - self.m_is_platform_viable.assert_called_with(dsrc.seed_dir) - self.assertFalse(ret) -- self.assertNotIn('agent_invoked', data) - # Assert that for non viable platforms, - # there is no communication with the Azure datasource. - self.assertEqual( -@@ -789,7 +779,6 @@ scbus-1 on xpt0 bus 0 - ret = dsrc.get_data() - self.m_is_platform_viable.assert_called_with(dsrc.seed_dir) - self.assertFalse(ret) -- self.assertNotIn('agent_invoked', data) - self.assertEqual( - 1, - m_report_failure.call_count) -@@ -806,7 +795,6 @@ scbus-1 on xpt0 bus 0 - 1, - m_crawl_metadata.call_count) - self.assertFalse(ret) -- self.assertNotIn('agent_invoked', data) - - def test_crawl_metadata_exception_should_report_failure_with_msg(self): - data = {} -@@ -1086,21 +1074,6 @@ scbus-1 on xpt0 bus 0 - self.assertTrue(os.path.isdir(self.waagent_d)) - self.assertEqual(stat.S_IMODE(os.stat(self.waagent_d).st_mode), 0o700) - -- def test_user_cfg_set_agent_command_plain(self): -- # set dscfg in via plaintext -- # we must have friendly-to-xml formatted plaintext in yaml_cfg -- # not all plaintext is expected to work. -- yaml_cfg = "{agent_command: my_command}\n" -- cfg = yaml.safe_load(yaml_cfg) -- odata = {'HostName': "myhost", 'UserName': "myuser", -- 'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}} -- data = {'ovfcontent': construct_valid_ovf_env(data=odata)} -- -- dsrc = self._get_ds(data) -- ret = self._get_and_setup(dsrc) -- self.assertTrue(ret) -- self.assertEqual(data['agent_invoked'], cfg['agent_command']) -- - @mock.patch('cloudinit.sources.DataSourceAzure.device_driver', - return_value=None) - def test_network_config_set_from_imds(self, m_driver): -@@ -1205,29 +1178,6 @@ scbus-1 on xpt0 bus 0 - dsrc.get_data() - self.assertEqual('eastus2', dsrc.region) - -- def test_user_cfg_set_agent_command(self): -- # set dscfg in via base64 encoded yaml -- cfg = {'agent_command': "my_command"} -- odata = {'HostName': "myhost", 'UserName': "myuser", -- 'dscfg': {'text': b64e(yaml.dump(cfg)), -- 'encoding': 'base64'}} -- data = {'ovfcontent': construct_valid_ovf_env(data=odata)} -- -- dsrc = self._get_ds(data) -- ret = self._get_and_setup(dsrc) -- self.assertTrue(ret) -- self.assertEqual(data['agent_invoked'], cfg['agent_command']) -- -- def test_sys_cfg_set_agent_command(self): -- sys_cfg = {'datasource': {'Azure': {'agent_command': '_COMMAND'}}} -- data = {'ovfcontent': construct_valid_ovf_env(data={}), -- 'sys_cfg': sys_cfg} -- -- dsrc = self._get_ds(data) -- ret = self._get_and_setup(dsrc) -- self.assertTrue(ret) -- self.assertEqual(data['agent_invoked'], '_COMMAND') -- - def test_sys_cfg_set_never_destroy_ntfs(self): - sys_cfg = {'datasource': {'Azure': { - 'never_destroy_ntfs': 'user-supplied-value'}}} -@@ -1311,51 +1261,6 @@ scbus-1 on xpt0 bus 0 - self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, mydata.encode('utf-8')) - -- def test_cfg_has_pubkeys_fingerprint(self): -- odata = {'HostName': "myhost", 'UserName': "myuser"} -- mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': ''}] -- pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist] -- data = {'ovfcontent': construct_valid_ovf_env(data=odata, -- pubkeys=pubkeys)} -- -- dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) -- ret = self._get_and_setup(dsrc) -- self.assertTrue(ret) -- for mypk in mypklist: -- self.assertIn(mypk, dsrc.cfg['_pubkeys']) -- self.assertIn('pubkey_from', dsrc.metadata['public-keys'][-1]) -- -- def test_cfg_has_pubkeys_value(self): -- # make sure that provided key is used over fingerprint -- odata = {'HostName': "myhost", 'UserName': "myuser"} -- mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': 'value1'}] -- pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist] -- data = {'ovfcontent': construct_valid_ovf_env(data=odata, -- pubkeys=pubkeys)} -- -- dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) -- ret = self._get_and_setup(dsrc) -- self.assertTrue(ret) -- -- for mypk in mypklist: -- self.assertIn(mypk, dsrc.cfg['_pubkeys']) -- self.assertIn(mypk['value'], dsrc.metadata['public-keys']) -- -- def test_cfg_has_no_fingerprint_has_value(self): -- # test value is used when fingerprint not provided -- odata = {'HostName': "myhost", 'UserName': "myuser"} -- mypklist = [{'fingerprint': None, 'path': 'path1', 'value': 'value1'}] -- pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist] -- data = {'ovfcontent': construct_valid_ovf_env(data=odata, -- pubkeys=pubkeys)} -- -- dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) -- ret = self._get_and_setup(dsrc) -- self.assertTrue(ret) -- -- for mypk in mypklist: -- self.assertIn(mypk['value'], dsrc.metadata['public-keys']) -- - def test_default_ephemeral_configs_ephemeral_exists(self): - # make sure the ephemeral configs are correct if disk present - odata = {} -@@ -1919,8 +1824,6 @@ class TestAzureBounce(CiTestCase): - with_logs = True - - def mock_out_azure_moving_parts(self): -- self.patches.enter_context( -- mock.patch.object(dsaz, 'invoke_agent')) - self.patches.enter_context( - mock.patch.object(dsaz.util, 'wait_for_files')) - self.patches.enter_context( --- -2.27.0 - diff --git a/SOURCES/ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch b/SOURCES/ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch deleted file mode 100644 index 44ad400..0000000 --- a/SOURCES/ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch +++ /dev/null @@ -1,97 +0,0 @@ -From 478709d7c157a085e3b2fee432e24978a3485234 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Wed, 20 Oct 2021 16:28:42 +0200 -Subject: [PATCH] cc_ssh.py: fix private key group owner and permissions - (#1070) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 32: cc_ssh.py: fix private key group owner and permissions (#1070) -RH-Commit: [1/1] 0382c3f671ae0fa9cab23dfad1f636967b012148 -RH-Bugzilla: 2013644 -RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -commit ee296ced9c0a61b1484d850b807c601bcd670ec1 -Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Tue Oct 19 21:32:10 2021 +0200 - - cc_ssh.py: fix private key group owner and permissions (#1070) - - When default host keys are created by sshd-keygen (/etc/ssh/ssh_host_*_key) - in RHEL/CentOS/Fedora, openssh it performs the following: - - # create new keys - if ! $KEYGEN -q -t $KEYTYPE -f $KEY -C '' -N '' >&/dev/null; then - exit 1 - fi - - # sanitize permissions - /usr/bin/chgrp ssh_keys $KEY - /usr/bin/chmod 640 $KEY - /usr/bin/chmod 644 $KEY.pub - Note that the group ssh_keys exists only in RHEL/CentOS/Fedora. - - Now that we disable sshd-keygen to allow only cloud-init to create - them, we miss the "sanitize permissions" part, where we set the group - owner as ssh_keys and the private key mode to 640. - - According to https://bugzilla.redhat.com/show_bug.cgi?id=2013644#c8, failing - to set group ownership and permissions like openssh does makes the RHEL openscap - tool generate an error. - - Signed-off-by: Emanuele Giuseppe Esposito eesposit@redhat.com - - RHBZ: 2013644 - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/config/cc_ssh.py | 7 +++++++ - cloudinit/util.py | 14 ++++++++++++++ - 2 files changed, 21 insertions(+) - -diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py -index 05a16dbc..4e986c55 100755 ---- a/cloudinit/config/cc_ssh.py -+++ b/cloudinit/config/cc_ssh.py -@@ -240,6 +240,13 @@ def handle(_name, cfg, cloud, log, _args): - try: - out, err = subp.subp(cmd, capture=True, env=lang_c) - sys.stdout.write(util.decode_binary(out)) -+ -+ gid = util.get_group_id("ssh_keys") -+ if gid != -1: -+ # perform same "sanitize permissions" as sshd-keygen -+ os.chown(keyfile, -1, gid) -+ os.chmod(keyfile, 0o640) -+ os.chmod(keyfile + ".pub", 0o644) - except subp.ProcessExecutionError as e: - err = util.decode_binary(e.stderr).lower() - if (e.exit_code == 1 and -diff --git a/cloudinit/util.py b/cloudinit/util.py -index 343976ad..fe37ae89 100644 ---- a/cloudinit/util.py -+++ b/cloudinit/util.py -@@ -1831,6 +1831,20 @@ def chmod(path, mode): - os.chmod(path, real_mode) - - -+def get_group_id(grp_name: str) -> int: -+ """ -+ Returns the group id of a group name, or -1 if no group exists -+ -+ @param grp_name: the name of the group -+ """ -+ gid = -1 -+ try: -+ gid = grp.getgrnam(grp_name).gr_gid -+ except KeyError: -+ LOG.debug("Group %s is not a valid group name", grp_name) -+ return gid -+ -+ - def get_permissions(path: str) -> int: - """ - Returns the octal permissions of the file/folder pointed by the path, --- -2.27.0 - diff --git a/SOURCES/ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch b/SOURCES/ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch deleted file mode 100644 index 9ea95c1..0000000 --- a/SOURCES/ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch +++ /dev/null @@ -1,87 +0,0 @@ -From ea83e72b335e652b080fda66a075c0d1322ed6dc Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Tue, 7 Dec 2021 10:00:41 +0100 -Subject: [PATCH] cloudinit/net: handle two different routes for the same ip - (#1124) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 39: cloudinit/net: handle two different routes for the same ip (#1124) -RH-Commit: [1/1] 6810dc29ce786fbca96d2033386aa69c6ab65997 -RH-Bugzilla: 2028028 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> - -commit 0e25076b34fa995161b83996e866c0974cee431f -Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Mon Dec 6 18:34:26 2021 +0100 - - cloudinit/net: handle two different routes for the same ip (#1124) - - If we set a dhcp server side like this: - $ cat /var/tmp/cloud-init/cloud-init-dhcp-f0rie5tm/dhcp.leases - lease { - ... - option classless-static-routes 31.169.254.169.254 0.0.0.0,31.169.254.169.254 - 10.112.143.127,22.10.112.140 0.0.0.0,0 10.112.140.1; - ... - } - cloud-init fails to configure the routes via 'ip route add' because to there are - two different routes for 169.254.169.254: - - $ ip -4 route add 192.168.1.1/32 via 0.0.0.0 dev eth0 - $ ip -4 route add 192.168.1.1/32 via 10.112.140.248 dev eth0 - - But NetworkManager can handle such scenario successfully as it uses "ip route append". - So change cloud-init to also use "ip route append" to fix the issue: - - $ ip -4 route append 192.168.1.1/32 via 0.0.0.0 dev eth0 - $ ip -4 route append 192.168.1.1/32 via 10.112.140.248 dev eth0 - - Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> - - RHBZ: #2003231 - -Conflicts: - cloudinit/net/tests/test_init.py: a mock call in - test_ephemeral_ipv4_network_with_rfc3442_static_routes is not - present downstream. - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/net/__init__.py | 2 +- - cloudinit/net/tests/test_init.py | 4 ++-- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py -index 385b7bcc..003efa2a 100644 ---- a/cloudinit/net/__init__.py -+++ b/cloudinit/net/__init__.py -@@ -1138,7 +1138,7 @@ class EphemeralIPv4Network(object): - if gateway != "0.0.0.0/0": - via_arg = ['via', gateway] - subp.subp( -- ['ip', '-4', 'route', 'add', net_address] + via_arg + -+ ['ip', '-4', 'route', 'append', net_address] + via_arg + - ['dev', self.interface], capture=True) - self.cleanup_cmds.insert( - 0, ['ip', '-4', 'route', 'del', net_address] + via_arg + -diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py -index 946f8ee2..2350837b 100644 ---- a/cloudinit/net/tests/test_init.py -+++ b/cloudinit/net/tests/test_init.py -@@ -719,10 +719,10 @@ class TestEphemeralIPV4Network(CiTestCase): - ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'], - capture=True), - mock.call( -- ['ip', '-4', 'route', 'add', '169.254.169.254/32', -+ ['ip', '-4', 'route', 'append', '169.254.169.254/32', - 'via', '192.168.2.1', 'dev', 'eth0'], capture=True), - mock.call( -- ['ip', '-4', 'route', 'add', '0.0.0.0/0', -+ ['ip', '-4', 'route', 'append', '0.0.0.0/0', - 'via', '192.168.2.1', 'dev', 'eth0'], capture=True)] - expected_teardown_calls = [ - mock.call( --- -2.27.0 - diff --git a/SOURCES/ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch b/SOURCES/ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch deleted file mode 100644 index f257a67..0000000 --- a/SOURCES/ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch +++ /dev/null @@ -1,173 +0,0 @@ -From 005d0a98c69d154a00e9fd599c7fbe5aef73c933 Mon Sep 17 00:00:00 2001 -From: Amy Chen <xiachen@redhat.com> -Date: Thu, 25 Nov 2021 18:30:48 +0800 -Subject: [PATCH] fix error on upgrade caused by new vendordata2 attributes - -RH-Author: xiachen <None> -RH-MergeRequest: 35: fix error on upgrade caused by new vendordata2 attributes -RH-Commit: [1/1] 9e00a7744838afbbdc5eb14628b7f572beba9f19 -RH-Bugzilla: 2021538 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Eduardo Otubo <otubo@redhat.com> -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> - -commit d132356cc361abef2d90d4073438f3ab759d5964 -Author: James Falcon <TheRealFalcon@users.noreply.github.com> -Date: Mon Apr 19 11:31:28 2021 -0500 - - fix error on upgrade caused by new vendordata2 attributes (#869) - - In #777, we added 'vendordata2' and 'vendordata2_raw' attributes to - the DataSource class, but didn't use the upgrade framework to deal - with an unpickle after upgrade. This commit adds the necessary - upgrade code. - - Additionally, added a smaller-scope upgrade test to our integration - tests that will be run on every CI run so we catch these issues - immediately in the future. - - LP: #1922739 - -Signed-off-by: Amy Chen <xiachen@redhat.com> ---- - cloudinit/sources/__init__.py | 12 +++++++++++- - cloudinit/tests/test_upgrade.py | 4 ++++ - tests/integration_tests/clouds.py | 4 ++-- - tests/integration_tests/test_upgrade.py | 25 ++++++++++++++++++++++++- - 4 files changed, 41 insertions(+), 4 deletions(-) - -diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py -index 1ad1880d..7d74f8d9 100644 ---- a/cloudinit/sources/__init__.py -+++ b/cloudinit/sources/__init__.py -@@ -24,6 +24,7 @@ from cloudinit import util - from cloudinit.atomic_helper import write_json - from cloudinit.event import EventType - from cloudinit.filters import launch_index -+from cloudinit.persistence import CloudInitPickleMixin - from cloudinit.reporting import events - - DSMODE_DISABLED = "disabled" -@@ -134,7 +135,7 @@ URLParams = namedtuple( - 'URLParms', ['max_wait_seconds', 'timeout_seconds', 'num_retries']) - - --class DataSource(metaclass=abc.ABCMeta): -+class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): - - dsmode = DSMODE_NETWORK - default_locale = 'en_US.UTF-8' -@@ -196,6 +197,8 @@ class DataSource(metaclass=abc.ABCMeta): - # non-root users - sensitive_metadata_keys = ('merged_cfg', 'security-credentials',) - -+ _ci_pkl_version = 1 -+ - def __init__(self, sys_cfg, distro, paths, ud_proc=None): - self.sys_cfg = sys_cfg - self.distro = distro -@@ -218,6 +221,13 @@ class DataSource(metaclass=abc.ABCMeta): - else: - self.ud_proc = ud_proc - -+ def _unpickle(self, ci_pkl_version: int) -> None: -+ """Perform deserialization fixes for Paths.""" -+ if not hasattr(self, 'vendordata2'): -+ self.vendordata2 = None -+ if not hasattr(self, 'vendordata2_raw'): -+ self.vendordata2_raw = None -+ - def __str__(self): - return type_utils.obj_name(self) - -diff --git a/cloudinit/tests/test_upgrade.py b/cloudinit/tests/test_upgrade.py -index f79a2536..fd3c5812 100644 ---- a/cloudinit/tests/test_upgrade.py -+++ b/cloudinit/tests/test_upgrade.py -@@ -43,3 +43,7 @@ class TestUpgrade: - def test_blacklist_drivers_set_on_networking(self, previous_obj_pkl): - """We always expect Networking.blacklist_drivers to be initialised.""" - assert previous_obj_pkl.distro.networking.blacklist_drivers is None -+ -+ def test_vendordata_exists(self, previous_obj_pkl): -+ assert previous_obj_pkl.vendordata2 is None -+ assert previous_obj_pkl.vendordata2_raw is None -diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py -index 9527a413..1d0b9d83 100644 ---- a/tests/integration_tests/clouds.py -+++ b/tests/integration_tests/clouds.py -@@ -100,14 +100,14 @@ class IntegrationCloud(ABC): - # Even if we're using the default key, it may still have a - # different name in the clouds, so we need to set it separately. - self.cloud_instance.key_pair.name = settings.KEYPAIR_NAME -- self._released_image_id = self._get_initial_image() -+ self.released_image_id = self._get_initial_image() - self.snapshot_id = None - - @property - def image_id(self): - if self.snapshot_id: - return self.snapshot_id -- return self._released_image_id -+ return self.released_image_id - - def emit_settings_to_log(self) -> None: - log.info( -diff --git a/tests/integration_tests/test_upgrade.py b/tests/integration_tests/test_upgrade.py -index c20cb3c1..48e0691b 100644 ---- a/tests/integration_tests/test_upgrade.py -+++ b/tests/integration_tests/test_upgrade.py -@@ -1,4 +1,5 @@ - import logging -+import os - import pytest - import time - from pathlib import Path -@@ -8,6 +9,8 @@ from tests.integration_tests.conftest import ( - get_validated_source, - session_start_time, - ) -+from tests.integration_tests.instances import CloudInitSource -+ - - log = logging.getLogger('integration_testing') - -@@ -63,7 +66,7 @@ def test_upgrade(session_cloud: IntegrationCloud): - return # type checking doesn't understand that skip raises - - launch_kwargs = { -- 'image_id': session_cloud._get_initial_image(), -+ 'image_id': session_cloud.released_image_id, - } - - image = ImageSpecification.from_os_image() -@@ -93,6 +96,26 @@ def test_upgrade(session_cloud: IntegrationCloud): - instance.install_new_cloud_init(source, take_snapshot=False) - instance.execute('hostname something-else') - _restart(instance) -+ assert instance.execute('cloud-init status --wait --long').ok - _output_to_compare(instance, after_path, netcfg_path) - - log.info('Wrote upgrade test logs to %s and %s', before_path, after_path) -+ -+ -+@pytest.mark.ci -+@pytest.mark.ubuntu -+def test_upgrade_package(session_cloud: IntegrationCloud): -+ if get_validated_source(session_cloud) != CloudInitSource.DEB_PACKAGE: -+ not_run_message = 'Test only supports upgrading to build deb' -+ if os.environ.get('TRAVIS'): -+ # If this isn't running on CI, we should know -+ pytest.fail(not_run_message) -+ else: -+ pytest.skip(not_run_message) -+ -+ launch_kwargs = {'image_id': session_cloud.released_image_id} -+ -+ with session_cloud.launch(launch_kwargs=launch_kwargs) as instance: -+ instance.install_deb() -+ instance.restart() -+ assert instance.execute('cloud-init status --wait --long').ok --- -2.27.0 - diff --git a/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch b/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch deleted file mode 100644 index be1e283..0000000 --- a/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch +++ /dev/null @@ -1,65 +0,0 @@ -From abf1adeae8211f5acd87dc63b03b2ed995047efd Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Thu, 20 May 2021 08:53:55 +0200 -Subject: [PATCH 1/2] rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and - set in cloud.cfg - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -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 <otubo@redhat.com> -RH-Acked-by: Cathy Avery <cavery@redhat.com> -RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -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 <eesposit@redhat.com> ---- - 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 43a1490c..2acf2615 100644 ---- a/cloudinit/settings.py -+++ b/cloudinit/settings.py -@@ -49,8 +49,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 9ecba215..cbee197a 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', '0', '2'] - resize_rootfs_tmp: /dev - ssh_deletekeys: 1 --ssh_genkeytypes: ~ -+ssh_genkeytypes: ['rsa', 'ecdsa', 'ed25519'] - syslog_fix_perms: ~ - disable_vmware_customization: false - --- -2.27.0 - 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..1385aae --- /dev/null +++ b/SOURCES/ci-setup.py-adjust-udev-rules-default-path-1513.patch @@ -0,0 +1,57 @@ +From ed7060ac1d5003f70fc3da4d6006a1a958a47b04 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Mon, 20 Jun 2022 10:31:14 +0200 +Subject: [PATCH 2/2] setup.py: adjust udev/rules default path (#1513) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 80: setup.py: adjust udev/rules default path (#1513) +RH-Commit: [2/2] 2cb64b004acbe1b6a30f943b0da51d2d1f2f0d50 (eesposit/cloud-init) +RH-Bugzilla: 2096269 +RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> +RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> + +commit 70715125f3af118ae242770e61064c24f41e9a02 +Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +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 <eesposit@redhat.com> + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + 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.31.1 + diff --git a/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch deleted file mode 100644 index bdec823..0000000 --- a/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch +++ /dev/null @@ -1,653 +0,0 @@ -From aeab67600eb2d5e483812620b56ce5fb031a57d6 Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Mon, 12 Jul 2021 21:47:37 +0200 -Subject: [PATCH] ssh-util: allow cloudinit to merge all ssh keys into a custom - user file, defined in AuthorizedKeysFile (#937) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 25: ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937) -RH-Commit: [1/1] 27bbe94f3b9dd8734865766bd30b06cff83383ab (eesposit/cloud-init) -RH-Bugzilla: 1862967 -RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -TESTED: By me and QA -BREW: 38030830 - -Conflicts: upstream patch modifies tests/integration_tests/util.py, that is -not present in RHEL. - -commit 9b52405c6f0de5e00d5ee9c1d13540425d8f6bf5 -Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Mon Jul 12 20:21:02 2021 +0200 - - ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937) - - This patch aims to fix LP1911680, by analyzing the files provided - in sshd_config and merge all keys into an user-specific file. Also - introduces additional tests to cover this specific case. - - The file is picked by analyzing the path given in AuthorizedKeysFile. - - If it points inside the current user folder (path is /home/user/*), it - means it is an user-specific file, so we can copy all user-keys there. - If it contains a %u or %h, it means that there will be a specific - authorized_keys file for each user, so we can copy all user-keys there. - If no path points to an user-specific file, for example when only - /etc/ssh/authorized_keys is given, default to ~/.ssh/authorized_keys. - Note that if there are more than a single user-specific file, the last - one will be picked. - - Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> - Co-authored-by: James Falcon <therealfalcon@gmail.com> - - LP: #1911680 - RHBZ:1862967 - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/ssh_util.py | 22 +- - .../assets/keys/id_rsa.test1 | 38 +++ - .../assets/keys/id_rsa.test1.pub | 1 + - .../assets/keys/id_rsa.test2 | 38 +++ - .../assets/keys/id_rsa.test2.pub | 1 + - .../assets/keys/id_rsa.test3 | 38 +++ - .../assets/keys/id_rsa.test3.pub | 1 + - .../modules/test_ssh_keysfile.py | 85 ++++++ - tests/unittests/test_sshutil.py | 246 +++++++++++++++++- - 9 files changed, 456 insertions(+), 14 deletions(-) - create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1 - create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1.pub - create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2 - create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2.pub - create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3 - create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3.pub - create mode 100644 tests/integration_tests/modules/test_ssh_keysfile.py - -diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py -index c08042d6..89057262 100644 ---- a/cloudinit/ssh_util.py -+++ b/cloudinit/ssh_util.py -@@ -252,13 +252,15 @@ def render_authorizedkeysfile_paths(value, homedir, username): - def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - (ssh_dir, pw_ent) = users_ssh_info(username) - default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys') -+ user_authorizedkeys_file = default_authorizedkeys_file - auth_key_fns = [] - with util.SeLinuxGuard(ssh_dir, recursive=True): - try: - ssh_cfg = parse_ssh_config_map(sshd_cfg_file) -+ key_paths = ssh_cfg.get("authorizedkeysfile", -+ "%h/.ssh/authorized_keys") - auth_key_fns = render_authorizedkeysfile_paths( -- ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"), -- pw_ent.pw_dir, username) -+ key_paths, pw_ent.pw_dir, username) - - except (IOError, OSError): - # Give up and use a default key filename -@@ -267,8 +269,22 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - "config from %r, using 'AuthorizedKeysFile' file " - "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) - -+ # check if one of the keys is the user's one -+ for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns): -+ if any([ -+ '%u' in key_path, -+ '%h' in key_path, -+ auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir)) -+ ]): -+ user_authorizedkeys_file = auth_key_fn -+ -+ if user_authorizedkeys_file != default_authorizedkeys_file: -+ LOG.debug( -+ "AuthorizedKeysFile has an user-specific authorized_keys, " -+ "using %s", user_authorizedkeys_file) -+ - # always store all the keys in the user's private file -- return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) -+ return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) - - - def setup_user_keys(keys, username, options=None): -diff --git a/tests/integration_tests/assets/keys/id_rsa.test1 b/tests/integration_tests/assets/keys/id_rsa.test1 -new file mode 100644 -index 00000000..bd4c822e ---- /dev/null -+++ b/tests/integration_tests/assets/keys/id_rsa.test1 -@@ -0,0 +1,38 @@ -+-----BEGIN OPENSSH PRIVATE KEY----- -+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn -+NhAAAAAwEAAQAAAYEAtRlG96aJ23URvAgO/bBsuLl+lquc350aSwV98/i8vlvOn5GVcHye -+t/rXQg4lZ4s0owG3kWyQFY8nvTk+G+UNU8fN0anAzBDi+4MzsejkF9scjTMFmXVrIpICqV -+3bYQNjPv6r+ubQdkD01du3eB9t5/zl84gtshp0hBdofyz8u1/A25s7fVU67GyI7PdKvaS+ -+yvJSInZnb2e9VQzfJC+qAnN7gUZatBKjdgUtJeiUUeDaVnaS17b0aoT9iBO0sIcQtOTBlY -+lCjFt1TAMLZ64Hj3SfGZB7Yj0Z+LzFB2IWX1zzsjI68YkYPKOSL/NYhQU9e55kJQ7WnngN -+HY/2n/A7dNKSFDmgM5c9IWgeZ7fjpsfIYAoJ/CAxFIND+PEHd1gCS6xoEhaUVyh5WH/Xkw -+Kv1nx4AiZ2BFCE+75kySRLZUJ+5y0r3DU5ktMXeURzVIP7pu0R8DCul+GU+M/+THyWtAEO -+geaNJ6fYpo2ipDhbmTYt3kk2lMIapRxGBFs+37sdAAAFgGGJssNhibLDAAAAB3NzaC1yc2 -+EAAAGBALUZRvemidt1EbwIDv2wbLi5fparnN+dGksFffP4vL5bzp+RlXB8nrf610IOJWeL -+NKMBt5FskBWPJ705PhvlDVPHzdGpwMwQ4vuDM7Ho5BfbHI0zBZl1ayKSAqld22EDYz7+q/ -+rm0HZA9NXbt3gfbef85fOILbIadIQXaH8s/LtfwNubO31VOuxsiOz3Sr2kvsryUiJ2Z29n -+vVUM3yQvqgJze4FGWrQSo3YFLSXolFHg2lZ2kte29GqE/YgTtLCHELTkwZWJQoxbdUwDC2 -+euB490nxmQe2I9Gfi8xQdiFl9c87IyOvGJGDyjki/zWIUFPXueZCUO1p54DR2P9p/wO3TS -+khQ5oDOXPSFoHme346bHyGAKCfwgMRSDQ/jxB3dYAkusaBIWlFcoeVh/15MCr9Z8eAImdg -+RQhPu+ZMkkS2VCfuctK9w1OZLTF3lEc1SD+6btEfAwrpfhlPjP/kx8lrQBDoHmjSen2KaN -+oqQ4W5k2Ld5JNpTCGqUcRgRbPt+7HQAAAAMBAAEAAAGBAJJCTOd70AC2ptEGbR0EHHqADT -+Wgefy7A94tHFEqxTy0JscGq/uCGimaY7kMdbcPXT59B4VieWeAC2cuUPP0ZHQSfS5ke7oT -+tU3N47U+0uBVbNS4rUAH7bOo2o9wptnOA5x/z+O+AARRZ6tEXQOd1oSy4gByLf2Wkh2QTi -+vP6Hln1vlFgKEzcXg6G8fN3MYWxKRhWmZM3DLERMvorlqqSBLcs5VvfZfLKcsKWTExioAq -+KgwEjYm8T9+rcpsw1xBus3j9k7wCI1Sus6PCDjq0pcYKLMYM7p8ygnU2tRYrOztdIxgWRA -+w/1oenm1Mqq2tV5xJcBCwCLOGe6SFwkIRywOYc57j5McH98Xhhg9cViyyBdXy/baF0mro+ -+qPhOsWDxqwD4VKZ9UmQ6O8kPNKcc7QcIpFJhcO0g9zbp/MT0KueaWYrTKs8y4lUkTT7Xz6 -++MzlR122/JwlAbBo6Y2kWtB+y+XwBZ0BfyJsm2czDhKm7OI5KfuBNhq0tFfKwOlYBq4QAA -+AMAyvUof1R8LLISkdO3EFTKn5RGNkPPoBJmGs6LwvU7NSjjLj/wPQe4jsIBc585tvbrddp -+60h72HgkZ5tqOfdeBYOKqX0qQQBHUEvI6M+NeQTQRev8bCHMLXQ21vzpClnrwNzlja359E -+uTRfiPRwIlyPLhOUiClBDSAnBI9h82Hkk3zzsQ/xGfsPB7iOjRbW69bMRSVCRpeweCVmWC -+77DTsEOq69V2TdljhQNIXE5OcOWonIlfgPiI74cdd+dLhzc/AAAADBAO1/JXd2kYiRyNkZ -+aXTLcwiSgBQIYbobqVP3OEtTclr0P1JAvby3Y4cCaEhkenx+fBqgXAku5lKM+U1Q9AEsMk -+cjIhaDpb43rU7GPjMn4zHwgGsEKd5pC1yIQ2PlK+cHanAdsDjIg+6RR+fuvid/mBeBOYXb -+Py0sa3HyekLJmCdx4UEyNASoiNaGFLQVAqo+RACsXy6VMxFH5dqDYlvwrfUQLwxJmse9Vb -+GEuuPAsklNugZqssC2XOIujFVUpslduQAAAMEAwzVHQVtsc3icCSzEAARpDTUdTbI29OhB -+/FMBnjzS9/3SWfLuBOSm9heNCHs2jdGNb8cPdKZuY7S9Fx6KuVUPyTbSSYkjj0F4fTeC9g -+0ym4p4UWYdF67WSWwLORkaG8K0d+G/CXkz8hvKUg6gcZWKBHAE1ROrHu1nsc8v7mkiKq4I -+bnTw5Q9TgjbWcQWtgPq0wXyyl/K8S1SFdkMCTOHDD0RQ+jTV2WNGVwFTodIRHenX+Rw2g4 -+CHbTWbsFrHR1qFAAAACmphbWVzQG5ld3Q= -+-----END OPENSSH PRIVATE KEY----- -diff --git a/tests/integration_tests/assets/keys/id_rsa.test1.pub b/tests/integration_tests/assets/keys/id_rsa.test1.pub -new file mode 100644 -index 00000000..3d2e26e1 ---- /dev/null -+++ b/tests/integration_tests/assets/keys/id_rsa.test1.pub -@@ -0,0 +1 @@ -+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1GUb3ponbdRG8CA79sGy4uX6Wq5zfnRpLBX3z+Ly+W86fkZVwfJ63+tdCDiVnizSjAbeRbJAVjye9OT4b5Q1Tx83RqcDMEOL7gzOx6OQX2xyNMwWZdWsikgKpXdthA2M+/qv65tB2QPTV27d4H23n/OXziC2yGnSEF2h/LPy7X8Dbmzt9VTrsbIjs90q9pL7K8lIidmdvZ71VDN8kL6oCc3uBRlq0EqN2BS0l6JRR4NpWdpLXtvRqhP2IE7SwhxC05MGViUKMW3VMAwtnrgePdJ8ZkHtiPRn4vMUHYhZfXPOyMjrxiRg8o5Iv81iFBT17nmQlDtaeeA0dj/af8Dt00pIUOaAzlz0haB5nt+Omx8hgCgn8IDEUg0P48Qd3WAJLrGgSFpRXKHlYf9eTAq/WfHgCJnYEUIT7vmTJJEtlQn7nLSvcNTmS0xd5RHNUg/um7RHwMK6X4ZT4z/5MfJa0AQ6B5o0np9imjaKkOFuZNi3eSTaUwhqlHEYEWz7fux0= test1@host -diff --git a/tests/integration_tests/assets/keys/id_rsa.test2 b/tests/integration_tests/assets/keys/id_rsa.test2 -new file mode 100644 -index 00000000..5854d901 ---- /dev/null -+++ b/tests/integration_tests/assets/keys/id_rsa.test2 -@@ -0,0 +1,38 @@ -+-----BEGIN OPENSSH PRIVATE KEY----- -+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn -+NhAAAAAwEAAQAAAYEAvK50D2PWOc4ikyHVRJS6tDhqzjL5cKiivID4p1X8BYCVw83XAEGO -+LnItUyVXHNADlh6fpVq1NY6A2JVtygoPF6ZFx8ph7IWMmnhDdnxLLyGsbhd1M1tiXJD/R+ -+3WnGHRJ4PKrQavMLgqHRrieV3QVVfjFSeo6jX/4TruP6ZmvITMZWJrXaGphxJ/pPykEdkO -+i8AmKU9FNviojyPS2nNtj9B/635IdgWvrd7Vf5Ycsw9MR55LWSidwa856RH62Yl6LpEGTH -+m1lJiMk1u88JPSqvohhaUkLKkFpcQwcB0m76W1KOyllJsmX8bNXrlZsI+WiiYI7Xl5vQm2 -+17DEuNeavtPAtDMxu8HmTg2UJ55Naxehbfe2lx2k5kYGGw3i1O1OVN2pZ2/OB71LucYd/5 -+qxPaz03wswcGOJYGPkNc40vdES/Scc7Yt8HsnZuzqkyOgzn0HiUCzoYUYLYTpLf+yGmwxS -+yAEY056aOfkCsboKHOKiOmlJxNaZZFQkX1evep4DAAAFgC7HMbUuxzG1AAAAB3NzaC1yc2 -+EAAAGBALyudA9j1jnOIpMh1USUurQ4as4y+XCooryA+KdV/AWAlcPN1wBBji5yLVMlVxzQ -+A5Yen6VatTWOgNiVbcoKDxemRcfKYeyFjJp4Q3Z8Sy8hrG4XdTNbYlyQ/0ft1pxh0SeDyq -+0GrzC4Kh0a4nld0FVX4xUnqOo1/+E67j+mZryEzGVia12hqYcSf6T8pBHZDovAJilPRTb4 -+qI8j0tpzbY/Qf+t+SHYFr63e1X+WHLMPTEeeS1koncGvOekR+tmJei6RBkx5tZSYjJNbvP -+CT0qr6IYWlJCypBaXEMHAdJu+ltSjspZSbJl/GzV65WbCPloomCO15eb0JttewxLjXmr7T -+wLQzMbvB5k4NlCeeTWsXoW33tpcdpOZGBhsN4tTtTlTdqWdvzge9S7nGHf+asT2s9N8LMH -+BjiWBj5DXONL3REv0nHO2LfB7J2bs6pMjoM59B4lAs6GFGC2E6S3/shpsMUsgBGNOemjn5 -+ArG6ChziojppScTWmWRUJF9Xr3qeAwAAAAMBAAEAAAGASj/kkEHbhbfmxzujL2/P4Sfqb+ -+aDXqAeGkwujbs6h/fH99vC5ejmSMTJrVSeaUo6fxLiBDIj6UWA0rpLEBzRP59BCpRL4MXV -+RNxav/+9nniD4Hb+ug0WMhMlQmsH71ZW9lPYqCpfOq7ec8GmqdgPKeaCCEspH7HMVhfYtd -+eHylwAC02lrpz1l5/h900sS5G9NaWR3uPA+xbzThDs4uZVkSidjlCNt1QZhDSSk7jA5n34 -+qJ5UTGu9WQDZqyxWKND+RIyQuFAPGQyoyCC1FayHO2sEhT5qHuumL14Mn81XpzoXFoKyql -+rhBDe+pHhKArBYt92Evch0k1ABKblFxtxLXcvk4Fs7pHi+8k4+Cnazej2kcsu1kURlMZJB -+w2QT/8BV4uImbH05LtyscQuwGzpIoxqrnHrvg5VbohStmhoOjYybzqqW3/M0qhkn5JgTiy -+dJcHRJisRnAcmbmEchYtLDi6RW1e022H4I9AFXQqyr5HylBq6ugtWcFCsrcX8ibZ8xAAAA -+wQCAOPgwae6yZLkrYzRfbxZtGKNmhpI0EtNSDCHYuQQapFZJe7EFENs/VAaIiiut0yajGj -+c3aoKcwGIoT8TUM8E3GSNW6+WidUOC7H6W+/6N2OYZHRBACGz820xO+UBCl2oSk+dLBlfr -+IQzBGUWn5uVYCs0/2nxfCdFyHtMK8dMF/ypbdG+o1rXz5y9b7PVG6Mn+o1Rjsdkq7VERmy -+Pukd8hwATOIJqoKl3TuFyBeYFLqe+0e7uTeswQFw17PF31VjAAAADBAOpJRQb8c6qWqsvv -+vkve0uMuL0DfWW0G6+SxjPLcV6aTWL5xu0Grd8uBxDkkHU/CDrAwpchXyuLsvbw21Eje/u -+U5k9nLEscWZwcX7odxlK+EfAY2Bf5+Hd9bH5HMzTRJH8KkWK1EppOLPyiDxz4LZGzPLVyv -+/1PgSuvXkSWk1KIE4SvSemyxGX2tPVI6uO+URqevfnPOS1tMB7BMQlgkR6eh4bugx9UYx9 -+mwlXonNa4dN0iQxZ7N4rKFBbT/uyB2bQAAAMEAzisnkD8k9Tn8uyhxpWLHwb03X4ZUUHDV -+zu15e4a8dZ+mM8nHO986913Xz5JujlJKkGwFTvgWkIiR2zqTEauZHARH7gANpaweTm6lPd -+E4p2S0M3ulY7xtp9lCFIrDhMPPkGq8SFZB6qhgucHcZSRLq6ZDou3S2IdNOzDTpBtkhRCS -+0zFcdTLh3zZweoy8HGbW36bwB6s1CIL76Pd4F64i0Ms9CCCU6b+E5ArFhYQIsXiDbgHWbD -+tZRSm2GEgnDGAvAAAACmphbWVzQG5ld3Q= -+-----END OPENSSH PRIVATE KEY----- -diff --git a/tests/integration_tests/assets/keys/id_rsa.test2.pub b/tests/integration_tests/assets/keys/id_rsa.test2.pub -new file mode 100644 -index 00000000..f3831a57 ---- /dev/null -+++ b/tests/integration_tests/assets/keys/id_rsa.test2.pub -@@ -0,0 +1 @@ -+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8rnQPY9Y5ziKTIdVElLq0OGrOMvlwqKK8gPinVfwFgJXDzdcAQY4uci1TJVcc0AOWHp+lWrU1joDYlW3KCg8XpkXHymHshYyaeEN2fEsvIaxuF3UzW2JckP9H7dacYdEng8qtBq8wuCodGuJ5XdBVV+MVJ6jqNf/hOu4/pma8hMxlYmtdoamHEn+k/KQR2Q6LwCYpT0U2+KiPI9Lac22P0H/rfkh2Ba+t3tV/lhyzD0xHnktZKJ3BrznpEfrZiXoukQZMebWUmIyTW7zwk9Kq+iGFpSQsqQWlxDBwHSbvpbUo7KWUmyZfxs1euVmwj5aKJgjteXm9CbbXsMS415q+08C0MzG7weZODZQnnk1rF6Ft97aXHaTmRgYbDeLU7U5U3alnb84HvUu5xh3/mrE9rPTfCzBwY4lgY+Q1zjS90RL9Jxzti3weydm7OqTI6DOfQeJQLOhhRgthOkt/7IabDFLIARjTnpo5+QKxugoc4qI6aUnE1plkVCRfV696ngM= test2@host -diff --git a/tests/integration_tests/assets/keys/id_rsa.test3 b/tests/integration_tests/assets/keys/id_rsa.test3 -new file mode 100644 -index 00000000..2596c762 ---- /dev/null -+++ b/tests/integration_tests/assets/keys/id_rsa.test3 -@@ -0,0 +1,38 @@ -+-----BEGIN OPENSSH PRIVATE KEY----- -+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn -+NhAAAAAwEAAQAAAYEApPG4MdkYQKD57/qreFrh9GRC22y66qZOWZWRjC887rrbvBzO69hV -+yJpTIXleJEvpWiHYcjMR5G6NNFsnNtZ4fxDqmSc4vcFj53JsE/XNqLKq6psXadCb5vkNpG -+bxA+Z5bJlzJ969PgJIIEbgc86sei4kgR2MuPWqtZbY5GkpNCTqWuLYeFK+14oFruA2nyWH -+9MOIRDHK/d597psHy+LTMtymO7ZPhO571abKw6jvvwiSeDxVE9kV7KAQIuM9/S3gftvgQQ -+ron3GL34pgmIabdSGdbfHqGDooryJhlbquJZELBN236KgRNTCAjVvUzjjQr1eRP3xssGwV -+O6ECBGCQLl/aYogAgtwnwj9iXqtfiLK3EwlgjquU4+JQ0CVtLhG3gIZB+qoMThco0pmHTr -+jtfQCwrztsBBFunSa2/CstuV1mQ5O5ZrZ6ACo9yPRBNkns6+CiKdtMtCtzi3k2RDz9jpYm -+Pcak03Lr7IkdC1Tp6+jA+//yPHSO1o4CqW89IQzNAAAFgEUd7lZFHe5WAAAAB3NzaC1yc2 -+EAAAGBAKTxuDHZGECg+e/6q3ha4fRkQttsuuqmTlmVkYwvPO6627wczuvYVciaUyF5XiRL -+6Voh2HIzEeRujTRbJzbWeH8Q6pknOL3BY+dybBP1zaiyquqbF2nQm+b5DaRm8QPmeWyZcy -+fevT4CSCBG4HPOrHouJIEdjLj1qrWW2ORpKTQk6lri2HhSvteKBa7gNp8lh/TDiEQxyv3e -+fe6bB8vi0zLcpju2T4Tue9WmysOo778Ikng8VRPZFeygECLjPf0t4H7b4EEK6J9xi9+KYJ -+iGm3UhnW3x6hg6KK8iYZW6riWRCwTdt+ioETUwgI1b1M440K9XkT98bLBsFTuhAgRgkC5f -+2mKIAILcJ8I/Yl6rX4iytxMJYI6rlOPiUNAlbS4Rt4CGQfqqDE4XKNKZh0647X0AsK87bA -+QRbp0mtvwrLbldZkOTuWa2egAqPcj0QTZJ7OvgoinbTLQrc4t5NkQ8/Y6WJj3GpNNy6+yJ -+HQtU6evowPv/8jx0jtaOAqlvPSEMzQAAAAMBAAEAAAGAGaqbdPZJNdVWzyb8g6/wtSzc0n -+Qq6dSTIJGLonq/So69HpqFAGIbhymsger24UMGvsXBfpO/1wH06w68HWZmPa+OMeLOi4iK -+WTuO4dQ/+l5DBlq32/lgKSLcIpb6LhcxEdsW9j9Mx1dnjc45owun/yMq/wRwH1/q/nLIsV -+JD3R9ZcGcYNDD8DWIm3D17gmw+qbG7hJES+0oh4n0xS2KyZpm7LFOEMDVEA8z+hE/HbryQ -+vjD1NC91n+qQWD1wKfN3WZDRwip3z1I5VHMpvXrA/spHpa9gzHK5qXNmZSz3/dfA1zHjCR -+2dHjJnrIUH8nyPfw8t+COC+sQBL3Nr0KUWEFPRM08cOcQm4ctzg17aDIZBONjlZGKlReR8 -+1zfAw84Q70q2spLWLBLXSFblHkaOfijEbejIbaz2UUEQT27WD7RHAORdQlkx7eitk66T9d -+DzIq/cpYhm5Fs8KZsh3PLldp9nsHbD2Oa9J9LJyI4ryuIW0mVwRdvPSiiYi3K+mDCpAAAA -+wBe+ugEEJ+V7orb1f4Zez0Bd4FNkEc52WZL4CWbaCtM+ZBg5KnQ6xW14JdC8IS9cNi/I5P -+yLsBvG4bWPLGgQruuKY6oLueD6BFnKjqF6ACUCiSQldh4BAW1nYc2U48+FFvo3ZQyudFSy -+QEFlhHmcaNMDo0AIJY5Xnq2BG3nEX7AqdtZ8hhenHwLCRQJatDwSYBHDpSDdh9vpTnGp/2 -+0jBz25Ko4UANzvSAc3sA4yN3jfpoM366TgdNf8x3g1v7yljQAAAMEA0HSQjzH5nhEwB58k -+mYYxnBYp1wb86zIuVhAyjZaeinvBQSTmLow8sXIHcCVuD3CgBezlU2SX5d9YuvRU9rcthi -+uzn4wWnbnzYy4SwzkMJXchUAkumFVD8Hq5TNPh2Z+033rLLE08EhYypSeVpuzdpFoStaS9 -+3DUZA2bR/zLZI9MOVZRUcYImNegqIjOYHY8Sbj3/0QPV6+WpUJFMPvvedWhfaOsRMTA6nr -+VLG4pxkrieVl0UtuRGbzD/exXhXVi7AAAAwQDKkJj4ez/+KZFYlZQKiV0BrfUFcgS6ElFM -+2CZIEagCtu8eedrwkNqx2FUX33uxdvUTr4c9I3NvWeEEGTB9pgD4lh1x/nxfuhyGXtimFM -+GnznGV9oyz0DmKlKiKSEGwWf5G+/NiiCwwVJ7wsQQm7TqNtkQ9b8MhWWXC7xlXKUs7dmTa -+e8AqAndCCMEnbS1UQFO/R5PNcZXkFWDggLQ/eWRYKlrXgdnUgH6h0saOcViKpNJBUXb3+x -+eauhOY52PS/BcAAAAKamFtZXNAbmV3dAE= -+-----END OPENSSH PRIVATE KEY----- -diff --git a/tests/integration_tests/assets/keys/id_rsa.test3.pub b/tests/integration_tests/assets/keys/id_rsa.test3.pub -new file mode 100644 -index 00000000..057db632 ---- /dev/null -+++ b/tests/integration_tests/assets/keys/id_rsa.test3.pub -@@ -0,0 +1 @@ -+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCk8bgx2RhAoPnv+qt4WuH0ZELbbLrqpk5ZlZGMLzzuutu8HM7r2FXImlMheV4kS+laIdhyMxHkbo00Wyc21nh/EOqZJzi9wWPncmwT9c2osqrqmxdp0Jvm+Q2kZvED5nlsmXMn3r0+AkggRuBzzqx6LiSBHYy49aq1ltjkaSk0JOpa4th4Ur7XigWu4DafJYf0w4hEMcr93n3umwfL4tMy3KY7tk+E7nvVpsrDqO+/CJJ4PFUT2RXsoBAi4z39LeB+2+BBCuifcYvfimCYhpt1IZ1t8eoYOiivImGVuq4lkQsE3bfoqBE1MICNW9TOONCvV5E/fGywbBU7oQIEYJAuX9piiACC3CfCP2Jeq1+IsrcTCWCOq5Tj4lDQJW0uEbeAhkH6qgxOFyjSmYdOuO19ALCvO2wEEW6dJrb8Ky25XWZDk7lmtnoAKj3I9EE2Sezr4KIp20y0K3OLeTZEPP2OliY9xqTTcuvsiR0LVOnr6MD7//I8dI7WjgKpbz0hDM0= test3@host -diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py -new file mode 100644 -index 00000000..f82d7649 ---- /dev/null -+++ b/tests/integration_tests/modules/test_ssh_keysfile.py -@@ -0,0 +1,85 @@ -+import paramiko -+import pytest -+from io import StringIO -+from paramiko.ssh_exception import SSHException -+ -+from tests.integration_tests.instances import IntegrationInstance -+from tests.integration_tests.util import get_test_rsa_keypair -+ -+TEST_USER1_KEYS = get_test_rsa_keypair('test1') -+TEST_USER2_KEYS = get_test_rsa_keypair('test2') -+TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3') -+ -+USERDATA = """\ -+#cloud-config -+bootcmd: -+ - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config -+ssh_authorized_keys: -+ - {default} -+users: -+- default -+- name: test_user1 -+ ssh_authorized_keys: -+ - {user1} -+- name: test_user2 -+ ssh_authorized_keys: -+ - {user2} -+""".format( # noqa: E501 -+ default=TEST_DEFAULT_KEYS.public_key, -+ user1=TEST_USER1_KEYS.public_key, -+ user2=TEST_USER2_KEYS.public_key, -+) -+ -+ -+@pytest.mark.ubuntu -+@pytest.mark.user_data(USERDATA) -+def test_authorized_keys(client: IntegrationInstance): -+ expected_keys = [ -+ ('test_user1', '/home/test_user1/.ssh/authorized_keys2', -+ TEST_USER1_KEYS), -+ ('test_user2', '/home/test_user2/.ssh/authorized_keys2', -+ TEST_USER2_KEYS), -+ ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2', -+ TEST_DEFAULT_KEYS), -+ ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS), -+ ] -+ -+ for user, filename, keys in expected_keys: -+ contents = client.read_from_file(filename) -+ if user in ['ubuntu', 'root']: -+ # Our personal public key gets added by pycloudlib -+ lines = contents.split('\n') -+ assert len(lines) == 2 -+ assert keys.public_key.strip() in contents -+ else: -+ assert contents.strip() == keys.public_key.strip() -+ -+ # Ensure we can actually connect -+ ssh = paramiko.SSHClient() -+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) -+ paramiko_key = paramiko.RSAKey.from_private_key(StringIO( -+ keys.private_key)) -+ -+ # Will fail with AuthenticationException if -+ # we cannot connect -+ ssh.connect( -+ client.instance.ip, -+ username=user, -+ pkey=paramiko_key, -+ look_for_keys=False, -+ allow_agent=False, -+ ) -+ -+ # Ensure other uses can't connect using our key -+ other_users = [u[0] for u in expected_keys if u[2] != keys] -+ for other_user in other_users: -+ with pytest.raises(SSHException): -+ print('trying to connect as {} with key from {}'.format( -+ other_user, user)) -+ ssh.connect( -+ client.instance.ip, -+ username=other_user, -+ pkey=paramiko_key, -+ look_for_keys=False, -+ allow_agent=False, -+ ) -diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py -index fd1d1bac..bcb8044f 100644 ---- a/tests/unittests/test_sshutil.py -+++ b/tests/unittests/test_sshutil.py -@@ -570,20 +570,33 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase): - ssh_util.render_authorizedkeysfile_paths( - "%h/.keys", "/homedirs/bobby", "bobby")) - -+ def test_all(self): -+ self.assertEqual( -+ ["/homedirs/bobby/.keys", "/homedirs/bobby/.secret/keys", -+ "/keys/path1", "/opt/bobby/keys"], -+ ssh_util.render_authorizedkeysfile_paths( -+ "%h/.keys .secret/keys /keys/path1 /opt/%u/keys", -+ "/homedirs/bobby", "bobby")) -+ - - class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - - @patch("cloudinit.ssh_util.pwd.getpwnam") - def test_multiple_authorizedkeys_file_order1(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby') -+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') - m_getpwnam.return_value = fpw -- authorized_keys = self.tmp_path('authorized_keys') -+ user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ -+ # /tmp/home2/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) - util.write_file(authorized_keys, VALID_CONTENT['rsa']) - -- user_keys = self.tmp_path('user_keys') -+ # /tmp/home2/bobby/.ssh/user_keys = dsa -+ user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) - util.write_file(user_keys, VALID_CONTENT['dsa']) - -- sshd_config = self.tmp_path('sshd_config') -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config', dir="/tmp") - util.write_file( - sshd_config, - "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) -@@ -593,33 +606,244 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - fpw.pw_name, sshd_config) - content = ssh_util.update_authorized_keys(auth_key_entries, []) - -- self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) -+ self.assertEqual(user_keys, auth_key_fn) - self.assertTrue(VALID_CONTENT['rsa'] in content) - self.assertTrue(VALID_CONTENT['dsa'] in content) - - @patch("cloudinit.ssh_util.pwd.getpwnam") - def test_multiple_authorizedkeys_file_order2(self, m_getpwnam): -- fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie') -+ fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') - m_getpwnam.return_value = fpw -- authorized_keys = self.tmp_path('authorized_keys') -+ user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ -+ # /tmp/home/suzie/.ssh/authorized_keys = rsa -+ authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) - util.write_file(authorized_keys, VALID_CONTENT['rsa']) - -- user_keys = self.tmp_path('user_keys') -+ # /tmp/home/suzie/.ssh/user_keys = dsa -+ user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) - util.write_file(user_keys, VALID_CONTENT['dsa']) - -- sshd_config = self.tmp_path('sshd_config') -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config', dir="/tmp") - util.write_file( - sshd_config, -- "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) -+ "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys) - ) - - (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -- fpw.pw_name, sshd_config -+ fpw.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ self.assertEqual(authorized_keys, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['rsa'] in content) -+ self.assertTrue(VALID_CONTENT['dsa'] in content) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam): -+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -+ m_getpwnam.return_value = fpw -+ user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ -+ # /tmp/home2/bobby/.ssh/authorized_keys = rsa -+ authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder) -+ util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ -+ # /tmp/home2/bobby/.ssh/user_keys = dsa -+ user_keys = self.tmp_path('user_keys', dir=user_ssh_folder) -+ util.write_file(user_keys, VALID_CONTENT['dsa']) -+ -+ # /tmp/etc/ssh/authorized_keys = ecdsa -+ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', -+ dir="/tmp") -+ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config', dir="/tmp") -+ util.write_file( -+ sshd_config, -+ "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, -+ user_keys, authorized_keys) -+ ) -+ -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ self.assertEqual(authorized_keys, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['rsa'] in content) -+ self.assertTrue(VALID_CONTENT['ecdsa'] in content) -+ self.assertTrue(VALID_CONTENT['dsa'] in content) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam): -+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -+ m_getpwnam.return_value = fpw -+ user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ -+ # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa -+ authorized_keys = self.tmp_path('authorized_keys2', -+ dir=user_ssh_folder) -+ util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ -+ # /tmp/home2/bobby/.ssh/user_keys3 = dsa -+ user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) -+ util.write_file(user_keys, VALID_CONTENT['dsa']) -+ -+ # /tmp/etc/ssh/authorized_keys = ecdsa -+ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', -+ dir="/tmp") -+ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config', dir="/tmp") -+ util.write_file( -+ sshd_config, -+ "AuthorizedKeysFile %s %s %s" % (authorized_keys_global, -+ authorized_keys, user_keys) -+ ) -+ -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ self.assertEqual(user_keys, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['rsa'] in content) -+ self.assertTrue(VALID_CONTENT['ecdsa'] in content) -+ self.assertTrue(VALID_CONTENT['dsa'] in content) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ def test_multiple_authorizedkeys_file_global(self, m_getpwnam): -+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -+ m_getpwnam.return_value = fpw -+ -+ # /tmp/etc/ssh/authorized_keys = rsa -+ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys', -+ dir="/tmp") -+ util.write_file(authorized_keys_global, VALID_CONTENT['rsa']) -+ -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config') -+ util.write_file( -+ sshd_config, -+ "AuthorizedKeysFile %s" % (authorized_keys_global) - ) -+ -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw.pw_name, sshd_config) - content = ssh_util.update_authorized_keys(auth_key_entries, []) - - self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) - self.assertTrue(VALID_CONTENT['rsa'] in content) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam): -+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby') -+ m_getpwnam.return_value = fpw -+ user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa -+ authorized_keys = self.tmp_path('authorized_keys2', -+ dir=user_ssh_folder) -+ util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ # /tmp/home2/bobby/.ssh/user_keys3 = dsa -+ user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) -+ util.write_file(user_keys, VALID_CONTENT['dsa']) -+ -+ fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie') -+ user_ssh_folder = "%s/.ssh" % fpw2.pw_dir -+ # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com -+ authorized_keys2 = self.tmp_path('authorized_keys2', -+ dir=user_ssh_folder) -+ util.write_file(authorized_keys2, -+ VALID_CONTENT['ssh-xmss@openssh.com']) -+ -+ # /tmp/etc/ssh/authorized_keys = ecdsa -+ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', -+ dir="/tmp") -+ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config', dir="/tmp") -+ util.write_file( -+ sshd_config, -+ "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" % -+ (authorized_keys_global, user_keys) -+ ) -+ -+ # process first user -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ self.assertEqual(user_keys, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['rsa'] in content) -+ self.assertTrue(VALID_CONTENT['ecdsa'] in content) -+ self.assertTrue(VALID_CONTENT['dsa'] in content) -+ self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content) -+ -+ m_getpwnam.return_value = fpw2 -+ # process second user -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw2.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ self.assertEqual(authorized_keys2, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content) -+ self.assertTrue(VALID_CONTENT['ecdsa'] in content) -+ self.assertTrue(VALID_CONTENT['dsa'] in content) -+ self.assertFalse(VALID_CONTENT['rsa'] in content) -+ -+ @patch("cloudinit.ssh_util.pwd.getpwnam") -+ def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam): -+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby') -+ m_getpwnam.return_value = fpw -+ user_ssh_folder = "%s/.ssh" % fpw.pw_dir -+ # /tmp/home/bobby/.ssh/authorized_keys2 = rsa -+ authorized_keys = self.tmp_path('authorized_keys2', -+ dir=user_ssh_folder) -+ util.write_file(authorized_keys, VALID_CONTENT['rsa']) -+ # /tmp/home/bobby/.ssh/user_keys3 = dsa -+ user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder) -+ util.write_file(user_keys, VALID_CONTENT['dsa']) -+ -+ fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy') -+ user_ssh_folder = "%s/.ssh" % fpw2.pw_dir -+ # /tmp/home/badguy/home/bobby = "" -+ authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy") -+ -+ # /tmp/etc/ssh/authorized_keys = ecdsa -+ authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2', -+ dir="/tmp") -+ util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa']) -+ -+ # /tmp/sshd_config -+ sshd_config = self.tmp_path('sshd_config', dir="/tmp") -+ util.write_file( -+ sshd_config, -+ "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" % -+ (authorized_keys_global, user_keys, authorized_keys2) -+ ) -+ -+ # process first user -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ self.assertEqual(user_keys, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['rsa'] in content) -+ self.assertTrue(VALID_CONTENT['ecdsa'] in content) -+ self.assertTrue(VALID_CONTENT['dsa'] in content) -+ -+ m_getpwnam.return_value = fpw2 -+ # process second user -+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -+ fpw2.pw_name, sshd_config) -+ content = ssh_util.update_authorized_keys(auth_key_entries, []) -+ -+ # badguy should not take the key from the other user! -+ self.assertEqual(authorized_keys2, auth_key_fn) -+ self.assertTrue(VALID_CONTENT['ecdsa'] in content) - self.assertTrue(VALID_CONTENT['dsa'] in content) -+ self.assertFalse(VALID_CONTENT['rsa'] in content) - - # vi: ts=4 expandtab --- -2.27.0 - diff --git a/SOURCES/ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch b/SOURCES/ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch deleted file mode 100644 index 13484d3..0000000 --- a/SOURCES/ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch +++ /dev/null @@ -1,85 +0,0 @@ -From 7d4e16bfc1cefbdd4d1477480b02b1d6c1399e4d Mon Sep 17 00:00:00 2001 -From: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Mon, 20 Sep 2021 12:16:36 +0200 -Subject: [PATCH] ssh_utils.py: ignore when sshd_config options are not - key/value pairs (#1007) - -RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-MergeRequest: 31: ssh_utils.py: ignore when sshd_config options are not key/value pairs (#1007) -RH-Commit: [1/1] 9007fb8a116e98036ff17df0168a76e9a5843671 (eesposit/cloud-init) -RH-Bugzilla: 1862933 -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> -RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com> - -TESTED: by me -BREW: 39832462 - -commit 2ce857248162957a785af61c135ca8433fdbbcde -Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> -Date: Wed Sep 8 02:08:36 2021 +0200 - - ssh_utils.py: ignore when sshd_config options are not key/value pairs (#1007) - - As specified in #LP 1845552, - In cloudinit/ssh_util.py, in parse_ssh_config_lines(), we attempt to - parse each line of sshd_config. This function expects each line to - be one of the following forms: - - \# comment - key value - key=value - - However, options like DenyGroups and DenyUsers are specified to - *optionally* accepts values in sshd_config. - Cloud-init should comply to this and skip the option if a value - is not provided. - - Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> - -Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> ---- - cloudinit/ssh_util.py | 8 +++++++- - tests/unittests/test_sshutil.py | 8 ++++++++ - 2 files changed, 15 insertions(+), 1 deletion(-) - -diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py -index 9ccadf09..33679dcc 100644 ---- a/cloudinit/ssh_util.py -+++ b/cloudinit/ssh_util.py -@@ -484,7 +484,13 @@ def parse_ssh_config_lines(lines): - try: - key, val = line.split(None, 1) - except ValueError: -- key, val = line.split('=', 1) -+ try: -+ key, val = line.split('=', 1) -+ except ValueError: -+ LOG.debug( -+ "sshd_config: option \"%s\" has no key/value pair," -+ " skipping it", line) -+ continue - ret.append(SshdConfigLine(line, key, val)) - return ret - -diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py -index a66788bf..08e20050 100644 ---- a/tests/unittests/test_sshutil.py -+++ b/tests/unittests/test_sshutil.py -@@ -525,6 +525,14 @@ class TestUpdateSshConfigLines(test_helpers.CiTestCase): - self.assertEqual([self.pwauth], result) - self.check_line(lines[-1], self.pwauth, "no") - -+ def test_option_without_value(self): -+ """Implementation only accepts key-value pairs.""" -+ extended_exlines = self.exlines.copy() -+ denyusers_opt = "DenyUsers" -+ extended_exlines.append(denyusers_opt) -+ lines = ssh_util.parse_ssh_config_lines(list(extended_exlines)) -+ self.assertNotIn(denyusers_opt, str(lines)) -+ - def test_single_option_updated(self): - """A single update should have change made and line updated.""" - opt, val = ("UsePAM", "no") --- -2.27.0 - diff --git a/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch b/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch deleted file mode 100644 index 5cf4671..0000000 --- a/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch +++ /dev/null @@ -1,369 +0,0 @@ -From 769b9f8c9b1ecc294a197575108ae7cb54ad7f4b Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo <otubo@redhat.com> -Date: Mon, 5 Jul 2021 14:13:45 +0200 -Subject: [PATCH] write passwords only to serial console, lock down - cloud-init-output.log (#847) - -RH-Author: Eduardo Otubo <otubo@redhat.com> -RH-MergeRequest: 21: write passwords only to serial console, lock down cloud-init-output.log (#847) -RH-Commit: [1/1] 8f30f2b7d0d6f9dca19994dbd0827b44e998f238 (otubo/cloud-init) -RH-Bugzilla: 1945891 -RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> -RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com> - -commit b794d426b9ab43ea9d6371477466070d86e10668 -Author: Daniel Watkins <oddbloke@ubuntu.com> -Date: Fri Mar 19 10:06:42 2021 -0400 - - write passwords only to serial console, lock down cloud-init-output.log (#847) - - Prior to this commit, when a user specified configuration which would - generate random passwords for users, cloud-init would cause those - passwords to be written to the serial console by emitting them on - stderr. In the default configuration, any stdout or stderr emitted by - cloud-init is also written to `/var/log/cloud-init-output.log`. This - file is world-readable, meaning that those randomly-generated passwords - were available to be read by any user with access to the system. This - presents an obvious security issue. - - This commit responds to this issue in two ways: - - * We address the direct issue by moving from writing the passwords to - sys.stderr to writing them directly to /dev/console (via - util.multi_log); this means that the passwords will never end up in - cloud-init-output.log - * To avoid future issues like this, we also modify the logging code so - that any files created in a log sink subprocess will only be - owner/group readable and, if it exists, will be owned by the adm - group. This results in `/var/log/cloud-init-output.log` no longer - being world-readable, meaning that if there are other parts of the - codebase that are emitting sensitive data intended for the serial - console, that data is no longer available to all users of the system. - - LP: #1918303 - -Signed-off-by: Eduardo Otubo <otubo@redhat.com> ---- - cloudinit/config/cc_set_passwords.py | 5 +- - cloudinit/config/tests/test_set_passwords.py | 40 +++++++++---- - cloudinit/tests/test_util.py | 56 +++++++++++++++++++ - cloudinit/util.py | 38 +++++++++++-- - .../modules/test_set_password.py | 24 ++++++++ - tests/integration_tests/test_logging.py | 22 ++++++++ - tests/unittests/test_util.py | 4 ++ - 7 files changed, 173 insertions(+), 16 deletions(-) - create mode 100644 tests/integration_tests/test_logging.py - -diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py -index d6b5682d..433de751 100755 ---- a/cloudinit/config/cc_set_passwords.py -+++ b/cloudinit/config/cc_set_passwords.py -@@ -78,7 +78,6 @@ password. - """ - - import re --import sys - - from cloudinit.distros import ug_util - from cloudinit import log as logging -@@ -214,7 +213,9 @@ def handle(_name, cfg, cloud, log, args): - if len(randlist): - blurb = ("Set the following 'random' passwords\n", - '\n'.join(randlist)) -- sys.stderr.write("%s\n%s\n" % blurb) -+ util.multi_log( -+ "%s\n%s\n" % blurb, stderr=False, fallback_to_stdout=False -+ ) - - if expire: - expired_users = [] -diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py -index daa1ef51..bbe2ee8f 100644 ---- a/cloudinit/config/tests/test_set_passwords.py -+++ b/cloudinit/config/tests/test_set_passwords.py -@@ -74,10 +74,6 @@ class TestSetPasswordsHandle(CiTestCase): - - with_logs = True - -- def setUp(self): -- super(TestSetPasswordsHandle, self).setUp() -- self.add_patch('cloudinit.config.cc_set_passwords.sys.stderr', 'm_err') -- - def test_handle_on_empty_config(self, *args): - """handle logs that no password has changed when config is empty.""" - cloud = self.tmp_cloud(distro='ubuntu') -@@ -129,10 +125,12 @@ class TestSetPasswordsHandle(CiTestCase): - mock.call(['pw', 'usermod', 'ubuntu', '-p', '01-Jan-1970'])], - m_subp.call_args_list) - -+ @mock.patch(MODPATH + "util.multi_log") - @mock.patch(MODPATH + "util.is_BSD") - @mock.patch(MODPATH + "subp.subp") -- def test_handle_on_chpasswd_list_creates_random_passwords(self, m_subp, -- m_is_bsd): -+ def test_handle_on_chpasswd_list_creates_random_passwords( -+ self, m_subp, m_is_bsd, m_multi_log -+ ): - """handle parses command set random passwords.""" - m_is_bsd.return_value = False - cloud = self.tmp_cloud(distro='ubuntu') -@@ -146,10 +144,32 @@ class TestSetPasswordsHandle(CiTestCase): - self.assertIn( - 'DEBUG: Handling input for chpasswd as list.', - self.logs.getvalue()) -- self.assertNotEqual( -- [mock.call(['chpasswd'], -- '\n'.join(valid_random_pwds) + '\n')], -- m_subp.call_args_list) -+ -+ self.assertEqual(1, m_subp.call_count) -+ args, _kwargs = m_subp.call_args -+ self.assertEqual(["chpasswd"], args[0]) -+ -+ stdin = args[1] -+ user_pass = { -+ user: password -+ for user, password -+ in (line.split(":") for line in stdin.splitlines()) -+ } -+ -+ self.assertEqual(1, m_multi_log.call_count) -+ self.assertEqual( -+ mock.call(mock.ANY, stderr=False, fallback_to_stdout=False), -+ m_multi_log.call_args -+ ) -+ -+ self.assertEqual(set(["root", "ubuntu"]), set(user_pass.keys())) -+ written_lines = m_multi_log.call_args[0][0].splitlines() -+ for password in user_pass.values(): -+ for line in written_lines: -+ if password in line: -+ break -+ else: -+ self.fail("Password not emitted to console") - - - # vi: ts=4 expandtab -diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py -index b7a302f1..e811917e 100644 ---- a/cloudinit/tests/test_util.py -+++ b/cloudinit/tests/test_util.py -@@ -851,4 +851,60 @@ class TestEnsureFile: - assert "ab" == kwargs["omode"] - - -+@mock.patch("cloudinit.util.grp.getgrnam") -+@mock.patch("cloudinit.util.os.setgid") -+@mock.patch("cloudinit.util.os.umask") -+class TestRedirectOutputPreexecFn: -+ """This tests specifically the preexec_fn used in redirect_output.""" -+ -+ @pytest.fixture(params=["outfmt", "errfmt"]) -+ def preexec_fn(self, request): -+ """A fixture to gather the preexec_fn used by redirect_output. -+ -+ This enables simpler direct testing of it, and parameterises any tests -+ using it to cover both the stdout and stderr code paths. -+ """ -+ test_string = "| piped output to invoke subprocess" -+ if request.param == "outfmt": -+ args = (test_string, None) -+ elif request.param == "errfmt": -+ args = (None, test_string) -+ with mock.patch("cloudinit.util.subprocess.Popen") as m_popen: -+ util.redirect_output(*args) -+ -+ assert 1 == m_popen.call_count -+ _args, kwargs = m_popen.call_args -+ assert "preexec_fn" in kwargs, "preexec_fn not passed to Popen" -+ return kwargs["preexec_fn"] -+ -+ def test_preexec_fn_sets_umask( -+ self, m_os_umask, _m_setgid, _m_getgrnam, preexec_fn -+ ): -+ """preexec_fn should set a mask that avoids world-readable files.""" -+ preexec_fn() -+ -+ assert [mock.call(0o037)] == m_os_umask.call_args_list -+ -+ def test_preexec_fn_sets_group_id_if_adm_group_present( -+ self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn -+ ): -+ """We should setgrp to adm if present, so files are owned by them.""" -+ fake_group = mock.Mock(gr_gid=mock.sentinel.gr_gid) -+ m_getgrnam.return_value = fake_group -+ -+ preexec_fn() -+ -+ assert [mock.call("adm")] == m_getgrnam.call_args_list -+ assert [mock.call(mock.sentinel.gr_gid)] == m_setgid.call_args_list -+ -+ def test_preexec_fn_handles_absent_adm_group_gracefully( -+ self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn -+ ): -+ """We should handle an absent adm group gracefully.""" -+ m_getgrnam.side_effect = KeyError("getgrnam(): name not found: 'adm'") -+ -+ preexec_fn() -+ -+ assert 0 == m_setgid.call_count -+ - # vi: ts=4 expandtab -diff --git a/cloudinit/util.py b/cloudinit/util.py -index 769f3425..4e0a72db 100644 ---- a/cloudinit/util.py -+++ b/cloudinit/util.py -@@ -359,7 +359,7 @@ def find_modules(root_dir): - - - def multi_log(text, console=True, stderr=True, -- log=None, log_level=logging.DEBUG): -+ log=None, log_level=logging.DEBUG, fallback_to_stdout=True): - if stderr: - sys.stderr.write(text) - if console: -@@ -368,7 +368,7 @@ def multi_log(text, console=True, stderr=True, - with open(conpath, 'w') as wfh: - wfh.write(text) - wfh.flush() -- else: -+ elif fallback_to_stdout: - # A container may lack /dev/console (arguably a container bug). If - # it does not exist, then write output to stdout. this will result - # in duplicate stderr and stdout messages if stderr was True. -@@ -623,6 +623,26 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None): - if not o_err: - o_err = sys.stderr - -+ # pylint: disable=subprocess-popen-preexec-fn -+ def set_subprocess_umask_and_gid(): -+ """Reconfigure umask and group ID to create output files securely. -+ -+ This is passed to subprocess.Popen as preexec_fn, so it is executed in -+ the context of the newly-created process. It: -+ -+ * sets the umask of the process so created files aren't world-readable -+ * if an adm group exists in the system, sets that as the process' GID -+ (so that the created file(s) are owned by root:adm) -+ """ -+ os.umask(0o037) -+ try: -+ group_id = grp.getgrnam("adm").gr_gid -+ except KeyError: -+ # No adm group, don't set a group -+ pass -+ else: -+ os.setgid(group_id) -+ - if outfmt: - LOG.debug("Redirecting %s to %s", o_out, outfmt) - (mode, arg) = outfmt.split(" ", 1) -@@ -632,7 +652,12 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None): - owith = "wb" - new_fp = open(arg, owith) - elif mode == "|": -- proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE) -+ proc = subprocess.Popen( -+ arg, -+ shell=True, -+ stdin=subprocess.PIPE, -+ preexec_fn=set_subprocess_umask_and_gid, -+ ) - new_fp = proc.stdin - else: - raise TypeError("Invalid type for output format: %s" % outfmt) -@@ -654,7 +679,12 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None): - owith = "wb" - new_fp = open(arg, owith) - elif mode == "|": -- proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE) -+ proc = subprocess.Popen( -+ arg, -+ shell=True, -+ stdin=subprocess.PIPE, -+ preexec_fn=set_subprocess_umask_and_gid, -+ ) - new_fp = proc.stdin - else: - raise TypeError("Invalid type for error format: %s" % errfmt) -diff --git a/tests/integration_tests/modules/test_set_password.py b/tests/integration_tests/modules/test_set_password.py -index b13f76fb..d7cf91a5 100644 ---- a/tests/integration_tests/modules/test_set_password.py -+++ b/tests/integration_tests/modules/test_set_password.py -@@ -116,6 +116,30 @@ class Mixin: - # Which are not the same - assert shadow_users["harry"] != shadow_users["dick"] - -+ def test_random_passwords_not_stored_in_cloud_init_output_log( -+ self, class_client -+ ): -+ """We should not emit passwords to the in-instance log file. -+ -+ LP: #1918303 -+ """ -+ cloud_init_output = class_client.read_from_file( -+ "/var/log/cloud-init-output.log" -+ ) -+ assert "dick:" not in cloud_init_output -+ assert "harry:" not in cloud_init_output -+ -+ def test_random_passwords_emitted_to_serial_console(self, class_client): -+ """We should emit passwords to the serial console. (LP: #1918303)""" -+ try: -+ console_log = class_client.instance.console_log() -+ except NotImplementedError: -+ # Assume that an exception here means that we can't use the console -+ # log -+ pytest.skip("NotImplementedError when requesting console log") -+ assert "dick:" in console_log -+ assert "harry:" in console_log -+ - def test_explicit_password_set_correctly(self, class_client): - """Test that an explicitly-specified password is set correctly.""" - shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client) -diff --git a/tests/integration_tests/test_logging.py b/tests/integration_tests/test_logging.py -new file mode 100644 -index 00000000..b31a0434 ---- /dev/null -+++ b/tests/integration_tests/test_logging.py -@@ -0,0 +1,22 @@ -+"""Integration tests relating to cloud-init's logging.""" -+ -+ -+class TestVarLogCloudInitOutput: -+ """Integration tests relating to /var/log/cloud-init-output.log.""" -+ -+ def test_var_log_cloud_init_output_not_world_readable(self, client): -+ """ -+ The log can contain sensitive data, it shouldn't be world-readable. -+ -+ LP: #1918303 -+ """ -+ # Check the file exists -+ assert client.execute("test -f /var/log/cloud-init-output.log").ok -+ -+ # Check its permissions are as we expect -+ perms, user, group = client.execute( -+ "stat -c %a:%U:%G /var/log/cloud-init-output.log" -+ ).split(":") -+ assert "640" == perms -+ assert "root" == user -+ assert "adm" == group -diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py -index 857629f1..e5292001 100644 ---- a/tests/unittests/test_util.py -+++ b/tests/unittests/test_util.py -@@ -572,6 +572,10 @@ class TestMultiLog(helpers.FilesystemMockingTestCase): - util.multi_log(logged_string) - self.assertEqual(logged_string, self.stdout.getvalue()) - -+ def test_logs_dont_go_to_stdout_if_fallback_to_stdout_is_false(self): -+ util.multi_log('something', fallback_to_stdout=False) -+ self.assertEqual('', self.stdout.getvalue()) -+ - def test_logs_go_to_log_if_given(self): - log = mock.MagicMock() - logged_string = 'something very important' --- -2.27.0 - diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index 33b0b70..58a1b58 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -5,8 +5,8 @@ %global debug_package %{nil} Name: cloud-init -Version: 21.1 -Release: 14%{?dist} +Version: 22.1 +Release: 5%{?dist} Summary: Cloud instance init scripts Group: System Environment/Base @@ -18,57 +18,33 @@ Source1: cloud-init-tmpfiles.conf Patch0001: 0001-Add-initial-redhat-setup.patch Patch0002: 0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch Patch0003: 0003-limit-permissions-on-def_log_file.patch -Patch0004: 0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch -Patch0005: 0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch -Patch0006: 0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch -Patch0007: 0007-Remove-race-condition-between-cloud-init-and-Network.patch -Patch0008: 0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch -Patch0009: 0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch -# For bz#1957532 - [cloud-init] From RHEL 82+ cloud-init no longer displays sshd keys fingerprints from instance launched from a backup image -Patch10: ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch -# For bz#1945891 - CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-8] -Patch11: ci-write-passwords-only-to-serial-console-lock-down-clo.patch -# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure -Patch12: ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch -# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure -Patch13: ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch -# For bz#1995840 - [cloudinit] Fix home permissions modified by ssh module -Patch14: ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch -# For bz#1862933 - cloud-init fails with ValueError: need more than 1 value to unpack[rhel-8] -Patch15: ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch -# For bz#2013644 - cloud-init fails to set host key permissions correctly -Patch16: ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch -# For bz#2021538 - cloud-init.service fails to start after package update -Patch17: ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch -# For bz#2028028 - [RHEL-8] Above 19.2 of cloud-init fails to configure routes when configuring static and default routes to the same destination IP -Patch18: ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch -# For bz#2039697 - [RHEL8] [Azure] cloud-init fails to configure the system -# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' -Patch20: ci-Datasource-for-VMware-953.patch -# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' -Patch21: ci-Change-netifaces-dependency-to-0.10.4-965.patch -# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' -Patch22: ci-Update-dscheck_VMware-s-rpctool-check-970.patch -# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' -Patch23: ci-Revert-unnecesary-lcase-in-ds-identify-978.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch24: ci-Add-flexibility-to-IMDS-api-version-793.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch25: ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch26: ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch27: ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch28: ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch29: ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch -# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata -Patch30: ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch -# For bz#2046540 - 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::". -Patch31: ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch -# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' - +Patch0004: 0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch +Patch0005: 0005-Remove-race-condition-between-cloud-init-and-Network.patch +Patch0006: 0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch +# For bz#2059872 - [RHEL-8]Rebase cloud-init from Fedora so it can configure networking using NM keyfiles +Patch7: ci-Add-native-NetworkManager-support-1224.patch +# For bz#2059872 - [RHEL-8]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#2082071 - Align cloud.cfg file and systemd with cloud-init upstream .tmpl files +Patch9: ci-Align-rhel-custom-files-with-upstream-1431.patch +# For bz#2082071 - Align cloud.cfg file and systemd with cloud-init upstream .tmpl files +Patch10: ci-Remove-rhel-specific-files.patch +# For bz#2082686 - [cloud][init] Add support for reading tags from instance metadata +Patch11: ci-Support-EC2-tags-in-instance-metadata-1309.patch +# For bz#2096269 - Adjust udev/rules default path[RHEL-8] +Patch12: ci-setup.py-adjust-udev-rules-default-path-1513.patch +# For bz#2107464 - [RHEL-8.7] Cannot run sysconfig when changing the priority of network renderers +# For bz#2110066 - DNS integration with OpenStack/cloud-init/NetworkManager is not working +# For bz#2117526 - [RHEL8.7] Revert patch of configuring networking by NM keyfiles +# For bz#2104393 - [RHEL-8.7]Failed to config static IP and IPv6 according to VMware Customization Config File +# For bz#2098624 - [RHEL-8.7] IPv6 not workable when cloud-init configure network using NM keyfiles +Patch13: ci-Revert-Add-native-NetworkManager-support-1224.patch +# For bz#2107464 - [RHEL-8.7] Cannot run sysconfig when changing the priority of network renderers +# For bz#2110066 - DNS integration with OpenStack/cloud-init/NetworkManager is not working +# For bz#2117526 - [RHEL8.7] Revert patch of configuring networking by NM keyfiles +# For bz#2104393 - [RHEL-8.7]Failed to config static IP and IPv6 according to VMware Customization Config File +# For bz#2098624 - [RHEL-8.7] IPv6 not workable when cloud-init configure network using NM keyfiles +Patch14: ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch BuildArch: noarch @@ -144,8 +120,6 @@ sed -i -e 's|#!/usr/bin/env python|#!/usr/bin/env python3|' \ %install %py3_install -- -python3 tools/render-cloudcfg --variant fedora > $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg - sed -i "s,@@PACKAGED_VERSION@@,%{version}-%{release}," $RPM_BUILD_ROOT/%{python3_sitelib}/cloudinit/version.py mkdir -p $RPM_BUILD_ROOT/var/lib/cloud @@ -155,9 +129,6 @@ mkdir -p $RPM_BUILD_ROOT/run/cloud-init mkdir -p $RPM_BUILD_ROOT/%{_tmpfilesdir} cp -p %{SOURCE1} $RPM_BUILD_ROOT/%{_tmpfilesdir}/%{name}.conf -# We supply our own config file since our software differs from Ubuntu's. -cp -p rhel/cloud.cfg $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg - mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d cp -p tools/21-cloudinit.conf $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d/21-cloudinit.conf @@ -165,17 +136,10 @@ cp -p tools/21-cloudinit.conf $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d/21-cloudi mv $RPM_BUILD_ROOT/etc/NetworkManager/dispatcher.d/hook-network-manager \ $RPM_BUILD_ROOT/etc/NetworkManager/dispatcher.d/cloud-init-azure-hook -# Install our own systemd units (rhbz#1440831) -mkdir -p $RPM_BUILD_ROOT%{_unitdir} -cp rhel/systemd/* $RPM_BUILD_ROOT%{_unitdir}/ - [ ! -d $RPM_BUILD_ROOT/usr/lib/systemd/system-generators ] && mkdir -p $RPM_BUILD_ROOT/usr/lib/systemd/system-generators python3 tools/render-cloudcfg --variant rhel systemd/cloud-init-generator.tmpl > $RPM_BUILD_ROOT/usr/lib/systemd/system-generators/cloud-init-generator chmod 755 $RPM_BUILD_ROOT/usr/lib/systemd/system-generators/cloud-init-generator -[ ! -d $RPM_BUILD_ROOT/usr/lib/%{name} ] && mkdir -p $RPM_BUILD_ROOT/usr/lib/%{name} -cp -p tools/ds-identify $RPM_BUILD_ROOT%{_libexecdir}/%{name}/ds-identify - # installing man pages mkdir -p ${RPM_BUILD_ROOT}%{_mandir}/man1/ for man in cloud-id.1 cloud-init.1 cloud-init-per.1; do @@ -231,7 +195,6 @@ fi %files %license LICENSE -%doc ChangeLog rhel/README.rhel %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg %dir %{_sysconfdir}/cloud/cloud.cfg.d %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/*.cfg @@ -241,6 +204,8 @@ fi %{_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 @@ -253,17 +218,62 @@ fi %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 -%{_sysconfdir}/bash_completion.d/cloud-init +%{_datadir}/bash-completion/completions/cloud-init %{_bindir}/cloud-id -%{_libexecdir}/%{name}/ds-identify /usr/lib/systemd/system-generators/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 Jon Maloy <jmaloy@redhat.com> - 22.1-5 +- ci-Revert-Add-native-NetworkManager-support-1224.patch [bz#2107464 bz#2110066 bz#2117526 bz#2104393 bz#2098624] +- ci-Revert-Use-Network-Manager-and-Netplan-as-default-re.patch [bz#2107464 bz#2110066 bz#2117526 bz#2104393 bz#2098624] +- Resolves: bz#2107464 + ([RHEL-8.7] Cannot run sysconfig when changing the priority of network renderers) +- Resolves: bz#2110066 + (DNS integration with OpenStack/cloud-init/NetworkManager is not working) +- Resolves: bz#2117526 + ([RHEL8.7] Revert patch of configuring networking by NM keyfiles) +- Resolves: bz#2104393 + ([RHEL-8.7]Failed to config static IP and IPv6 according to VMware Customization Config File) +- Resolves: bz#2098624 + ([RHEL-8.7] IPv6 not workable when cloud-init configure network using NM keyfiles) + +* Tue Jul 12 2022 Miroslav Rezanina <mrezanin@redhat.com> - 22.1-4 +- ci-cloud-init.spec-adjust-path-for-66-azure-ephemeral.r.patch [bz#2096269] +- ci-setup.py-adjust-udev-rules-default-path-1513.patch [bz#2096269] +- Resolves: bz#2096269 + (Adjust udev/rules default path[RHEL-8]) + +* Thu Jun 23 2022 Jon Maloy <jmaloy@redhat.com> - 22.1-3 +- ci-Support-EC2-tags-in-instance-metadata-1309.patch [bz#2082686] +- Resolves: bz#2082686 + ([cloud][init] Add support for reading tags from instance metadata) + +* Tue May 31 2022 Jon Maloy <jmaloy@redhat.com> - 22.1-2 +- ci-Add-native-NetworkManager-support-1224.patch [bz#2059872] +- ci-Use-Network-Manager-and-Netplan-as-default-renderers.patch [bz#2059872] +- ci-Align-rhel-custom-files-with-upstream-1431.patch [bz#2082071] +- ci-Remove-rhel-specific-files.patch [bz#2082071] +- Resolves: bz#2059872 + ([RHEL-8]Rebase cloud-init from Fedora so it can configure networking using NM keyfiles) +- Resolves: bz#2082071 + (Align cloud.cfg file and systemd with cloud-init upstream .tmpl files) + +* Mon Apr 25 2022 Amy Chen <xiachen@redhat.com> - 22.1-1 +- Rebaes to 22.1 [bz#2065544] +- Resolves: bz#2065544 + ([RHEL-8.7.0] cloud-init rebase to 22.1) + +* Fri Apr 01 2022 Camilla Conte <cconte@redhat.com> - 21.1-15 +- ci-Detect-a-Python-version-change-and-clear-the-cache-8.patch [bz#1935826] +- ci-Fix-MIME-policy-failure-on-python-version-upgrade-93.patch [bz#1935826] + * Fri Feb 25 2022 Jon Maloy <jmaloy@redhat.com> - 21.1-14 - ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch [bz#2046540] - Resolves: bz#2046540 @@ -350,6 +360,17 @@ fi - Resolves: bz#1958174 ([RHEL-8.5.0] Rebase cloud-init to 21.1) +* Thu May 13 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8_4.3 +- ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch [bz#1957135] +- ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch [bz#1957135] +- Resolves: bz#1957135 + (Intermittent failure to start cloud-init due to failure to detect macs [rhel-8.4.0.z]) + +* Tue Apr 06 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8_4.1 +- ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch [bz#1942699] +- Resolves: bz#1942699 + ([Aliyun][RHEL8.4][cloud-init] cloud-init service failed to start with Alibaba instance [rhel-8.4.0.z]) + * Tue Feb 02 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8 - ci-fix-a-typo-in-man-page-cloud-init.1-752.patch [bz#1913127] - Resolves: bz#1913127