diff --git a/.cloud-init.metadata b/.cloud-init.metadata index 245563f..6803c51 100644 --- a/.cloud-init.metadata +++ b/.cloud-init.metadata @@ -1 +1 @@ -cbde66f717b7883c4ab64b145042de54f131afab SOURCES/cloud-init-20.3.tar.gz +2ae378aa2ae23b34b0ff123623ba5e2fbdc4928d SOURCES/cloud-init-21.1.tar.gz diff --git a/.gitignore b/.gitignore index e8608c9..103bcf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/cloud-init-20.3.tar.gz +SOURCES/cloud-init-21.1.tar.gz diff --git a/SOURCES/0001-Add-initial-redhat-setup.patch b/SOURCES/0001-Add-initial-redhat-setup.patch index 6f85c2d..b67fcae 100644 --- a/SOURCES/0001-Add-initial-redhat-setup.patch +++ b/SOURCES/0001-Add-initial-redhat-setup.patch @@ -1,8 +1,18 @@ -From 25ea7a28d69518319ae1ed1b3cd510147868fd29 Mon Sep 17 00:00:00 2001 +From 074cb9b011623849cfa95c1d7cc813bb28f03ff0 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:49:36 +0200 +Date: Fri, 7 May 2021 13:36:03 +0200 Subject: Add initial redhat setup +Merged patches (21.1): +- 915d30ad Change gating file to correct rhel version +- 311f318d Removing net-tools dependency +- 74731806 Adding man pages to Red Hat spec file +- 758d333d Removing blocking test from yaml configuration file +- c7e7c59c Changing permission of cloud-init-generator to 755 +- 8b85abbb Installing man pages in the correct place with correct permissions +- c6808d8d Fix unit failure of cloud-final.service if NetworkManager was not present. +- 11866ef6 Report full specific version with "cloud-init --version" + Rebase notes (18.5): - added bash_completition file - added cloud-id file @@ -33,36 +43,36 @@ setup.py: Signed-off-by: Eduardo Otubo --- .gitignore | 1 + - cloudinit/config/cc_chef.py | 67 ++++- + cloudinit/config/cc_chef.py | 67 +++- cloudinit/settings.py | 7 +- redhat/.gitignore | 1 + - redhat/Makefile | 71 +++++ - redhat/Makefile.common | 37 +++ + redhat/Makefile | 71 ++++ + redhat/Makefile.common | 37 ++ redhat/cloud-init-tmpfiles.conf | 1 + - redhat/cloud-init.spec.template | 517 ++++++++++++++++++++++++++++++++++ - redhat/gating.yaml | 9 + + redhat/cloud-init.spec.template | 530 ++++++++++++++++++++++++++ + 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/git-backport-diff | 327 ++++++++++++++++ + redhat/scripts/git-compile-check | 215 +++++++++++ + redhat/scripts/process-patches.sh | 77 ++++ redhat/scripts/tarball_checksum.sh | 3 + rhel/README.rhel | 5 + rhel/cloud-init-tmpfiles.conf | 1 + - rhel/cloud.cfg | 69 +++++ - rhel/systemd/cloud-config.service | 18 ++ + rhel/cloud.cfg | 69 ++++ + rhel/systemd/cloud-config.service | 18 + rhel/systemd/cloud-config.target | 11 + - rhel/systemd/cloud-final.service | 19 ++ + rhel/systemd/cloud-final.service | 24 ++ rhel/systemd/cloud-init-local.service | 31 ++ rhel/systemd/cloud-init.service | 25 ++ rhel/systemd/cloud-init.target | 7 + setup.py | 23 +- tools/read-version | 28 +- - 30 files changed, 1562 insertions(+), 50 deletions(-) + 30 files changed, 1579 insertions(+), 50 deletions(-) create mode 100644 redhat/.gitignore create mode 100644 redhat/Makefile create mode 100644 redhat/Makefile.common @@ -90,7 +100,7 @@ Signed-off-by: Eduardo Otubo create mode 100644 rhel/systemd/cloud-init.target diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py -index aaf7136..97ef649 100644 +index aaf71366..97ef649a 100644 --- a/cloudinit/config/cc_chef.py +++ b/cloudinit/config/cc_chef.py @@ -6,7 +6,70 @@ @@ -175,10 +185,10 @@ index aaf7136..97ef649 100644 REQUIRED_CHEF_DIRS = tuple([ '/etc/chef', diff --git a/cloudinit/settings.py b/cloudinit/settings.py -index ca4ffa8..3a04a58 100644 +index 91e1bfe7..e690c0fd 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py -@@ -46,13 +46,16 @@ CFG_BUILTIN = { +@@ -47,13 +47,16 @@ CFG_BUILTIN = { ], 'def_log_file': '/var/log/cloud-init.log', 'log_cfgs': [], @@ -199,7 +209,7 @@ index ca4ffa8..3a04a58 100644 'vendor_data': {'enabled': True, 'prefix': []}, diff --git a/rhel/README.rhel b/rhel/README.rhel new file mode 100644 -index 0000000..aa29630 +index 00000000..aa29630d --- /dev/null +++ b/rhel/README.rhel @@ -0,0 +1,5 @@ @@ -210,14 +220,14 @@ index 0000000..aa29630 + - grub_dpkg diff --git a/rhel/cloud-init-tmpfiles.conf b/rhel/cloud-init-tmpfiles.conf new file mode 100644 -index 0000000..0c6d2a3 +index 00000000..0c6d2a3b --- /dev/null +++ b/rhel/cloud-init-tmpfiles.conf @@ -0,0 +1 @@ +d /run/cloud-init 0700 root root - - diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg new file mode 100644 -index 0000000..82e8bf6 +index 00000000..82e8bf62 --- /dev/null +++ b/rhel/cloud.cfg @@ -0,0 +1,69 @@ @@ -292,7 +302,7 @@ index 0000000..82e8bf6 +# vim:syntax=yaml diff --git a/rhel/systemd/cloud-config.service b/rhel/systemd/cloud-config.service new file mode 100644 -index 0000000..f3dcd4b +index 00000000..f3dcd4be --- /dev/null +++ b/rhel/systemd/cloud-config.service @@ -0,0 +1,18 @@ @@ -316,7 +326,7 @@ index 0000000..f3dcd4b +WantedBy=cloud-init.target diff --git a/rhel/systemd/cloud-config.target b/rhel/systemd/cloud-config.target new file mode 100644 -index 0000000..ae9b7d0 +index 00000000..ae9b7d02 --- /dev/null +++ b/rhel/systemd/cloud-config.target @@ -0,0 +1,11 @@ @@ -333,10 +343,10 @@ index 0000000..ae9b7d0 +After=cloud-init-local.service cloud-init.service diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service new file mode 100644 -index 0000000..739b7e3 +index 00000000..e281c0cf --- /dev/null +++ b/rhel/systemd/cloud-final.service -@@ -0,0 +1,19 @@ +@@ -0,0 +1,24 @@ +[Unit] +Description=Execute cloud user/final scripts +After=network-online.target cloud-config.service rc-local.service @@ -350,6 +360,11 @@ index 0000000..739b7e3 +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 @@ -358,7 +373,7 @@ index 0000000..739b7e3 +WantedBy=cloud-init.target diff --git a/rhel/systemd/cloud-init-local.service b/rhel/systemd/cloud-init-local.service new file mode 100644 -index 0000000..8f9f6c9 +index 00000000..8f9f6c9f --- /dev/null +++ b/rhel/systemd/cloud-init-local.service @@ -0,0 +1,31 @@ @@ -395,7 +410,7 @@ index 0000000..8f9f6c9 +WantedBy=cloud-init.target diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service new file mode 100644 -index 0000000..d0023a0 +index 00000000..d0023a05 --- /dev/null +++ b/rhel/systemd/cloud-init.service @@ -0,0 +1,25 @@ @@ -426,7 +441,7 @@ index 0000000..d0023a0 +WantedBy=cloud-init.target diff --git a/rhel/systemd/cloud-init.target b/rhel/systemd/cloud-init.target new file mode 100644 -index 0000000..083c3b6 +index 00000000..083c3b6f --- /dev/null +++ b/rhel/systemd/cloud-init.target @@ -0,0 +1,7 @@ @@ -438,7 +453,7 @@ index 0000000..083c3b6 +Description=Cloud-init target +After=multi-user.target diff --git a/setup.py b/setup.py -index cbacf48..d5cd01a 100755 +index cbacf48e..d5cd01a4 100755 --- a/setup.py +++ b/setup.py @@ -125,14 +125,6 @@ INITSYS_FILES = { @@ -503,7 +518,7 @@ index cbacf48..d5cd01a 100755 'console_scripts': [ 'cloud-init = cloudinit.cmd.main:main', diff --git a/tools/read-version b/tools/read-version -index 02c9064..79755f7 100755 +index 02c90643..79755f78 100755 --- a/tools/read-version +++ b/tools/read-version @@ -71,32 +71,8 @@ version_long = None @@ -542,5 +557,5 @@ index 02c9064..79755f7 100755 # version is X.Y.Z[+xxx.gHASH] # version_long is None or X.Y.Z-xxx-gHASH -- -1.8.3.1 +2.27.0 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 ffa06c2..3dc704f 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,6 +1,6 @@ -From d9024cd3bd3bf09b05eb75ba3d81bd15f519c9f8 Mon Sep 17 00:00:00 2001 +From 472c2b5d4342b6ab6ce1584dc39bed0e6c1ca2e7 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:49:46 +0200 +Date: Fri, 7 May 2021 13:36:06 +0200 Subject: Do not write NM_CONTROLLED=no in generated interface config files Conflicts 20.3: @@ -13,14 +13,14 @@ Signed-off-by: Eduardo Otubo Signed-off-by: Ryan McCabe --- cloudinit/net/sysconfig.py | 2 +- - tests/unittests/test_net.py | 30 ------------------------------ - 2 files changed, 1 insertion(+), 31 deletions(-) + tests/unittests/test_net.py | 28 ---------------------------- + 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 0a5d481..23e467d 100644 +index 99a4bae4..3d276666 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py -@@ -277,7 +277,7 @@ class Renderer(renderer.Renderer): +@@ -289,7 +289,7 @@ class Renderer(renderer.Renderer): # details about this) iface_defaults = { @@ -30,7 +30,7 @@ index 0a5d481..23e467d 100644 'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'}, } diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index 54cc846..9985a97 100644 +index 38d934d4..c67b5fcc 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -535,7 +535,6 @@ GATEWAY=172.19.3.254 @@ -49,15 +49,15 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -754,7 +752,6 @@ IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64" - IPV6INIT=yes +@@ -756,7 +754,6 @@ IPV6_AUTOCONF=no IPV6_DEFAULTGW=2001:DB8::1 + IPV6_FORCE_ACCEPT_RA=no NETMASK=255.255.252.0 -NM_CONTROLLED=no ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -882,7 +879,6 @@ NETWORK_CONFIGS = { +@@ -884,7 +881,6 @@ NETWORK_CONFIGS = { BOOTPROTO=none DEVICE=eth1 HWADDR=cf:d6:af:48:e8:80 @@ -65,7 +65,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no"""), -@@ -899,7 +895,6 @@ NETWORK_CONFIGS = { +@@ -901,7 +897,6 @@ NETWORK_CONFIGS = { IPADDR=192.168.21.3 NETMASK=255.255.255.0 METRIC=10000 @@ -73,15 +73,15 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no"""), -@@ -1028,7 +1023,6 @@ NETWORK_CONFIGS = { - IPV6ADDR=2001:1::1/64 - IPV6INIT=yes +@@ -1032,7 +1027,6 @@ NETWORK_CONFIGS = { + IPV6_AUTOCONF=no + IPV6_FORCE_ACCEPT_RA=no NETMASK=255.255.255.0 - NM_CONTROLLED=no ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -1622,7 +1616,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1737,7 +1731,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 @@ -89,15 +89,15 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Bond USERCTL=no"""), -@@ -1630,7 +1623,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1745,7 +1738,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true BOOTPROTO=dhcp DEVICE=bond0.200 DHCLIENT_SET_DEFAULT_ROUTE=no - NM_CONTROLLED=no ONBOOT=yes PHYSDEV=bond0 - TYPE=Ethernet -@@ -1647,7 +1639,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + USERCTL=no +@@ -1763,7 +1755,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,7 +105,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes PRIO=22 STP=no -@@ -1657,7 +1648,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1773,7 +1764,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 @@ -113,15 +113,15 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no"""), -@@ -1674,7 +1664,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1790,7 +1780,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true MTU=1500 NETMASK=255.255.255.0 NETMASK1=255.255.255.0 - NM_CONTROLLED=no ONBOOT=yes PHYSDEV=eth0 - TYPE=Ethernet -@@ -1685,7 +1674,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true + USERCTL=no +@@ -1800,7 +1789,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 +129,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -1695,7 +1683,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1810,7 +1798,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,7 +137,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -1705,7 +1692,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1820,7 +1807,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 @@ -145,7 +145,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no"""), -@@ -1714,7 +1700,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1829,7 +1815,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 @@ -153,7 +153,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no"""), -@@ -1723,7 +1708,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -1838,7 +1823,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 @@ -161,7 +161,7 @@ index 54cc846..9985a97 100644 ONBOOT=no TYPE=Ethernet USERCTL=no"""), -@@ -2177,7 +2161,6 @@ iface bond0 inet6 static +@@ -2294,7 +2278,6 @@ iface bond0 inet6 static MTU=9000 NETMASK=255.255.255.0 NETMASK1=255.255.255.0 @@ -169,7 +169,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Bond USERCTL=no -@@ -2187,7 +2170,6 @@ iface bond0 inet6 static +@@ -2304,7 +2287,6 @@ iface bond0 inet6 static DEVICE=bond0s0 HWADDR=aa:bb:cc:dd:e8:00 MASTER=bond0 @@ -177,7 +177,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -2209,7 +2191,6 @@ iface bond0 inet6 static +@@ -2326,7 +2308,6 @@ iface bond0 inet6 static DEVICE=bond0s1 HWADDR=aa:bb:cc:dd:e8:01 MASTER=bond0 @@ -185,7 +185,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes SLAVE=yes TYPE=Ethernet -@@ -2266,7 +2247,6 @@ iface bond0 inet6 static +@@ -2383,7 +2364,6 @@ iface bond0 inet6 static BOOTPROTO=none DEVICE=en0 HWADDR=aa:bb:cc:dd:e8:00 @@ -193,15 +193,15 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no"""), -@@ -2283,7 +2263,6 @@ iface bond0 inet6 static +@@ -2402,7 +2382,6 @@ iface bond0 inet6 static MTU=2222 NETMASK=255.255.255.0 NETMASK1=255.255.255.0 - NM_CONTROLLED=no ONBOOT=yes PHYSDEV=en0 - TYPE=Ethernet -@@ -2349,7 +2328,6 @@ iface bond0 inet6 static + USERCTL=no +@@ -2467,7 +2446,6 @@ iface bond0 inet6 static DEVICE=br0 IPADDR=192.168.2.2 NETMASK=255.255.255.0 @@ -209,23 +209,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes PRIO=22 STP=no -@@ -2363,7 +2341,6 @@ iface bond0 inet6 static - HWADDR=52:54:00:12:34:00 - IPV6ADDR=2001:1::100/96 - IPV6INIT=yes -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -2375,7 +2352,6 @@ iface bond0 inet6 static - HWADDR=52:54:00:12:34:01 - IPV6ADDR=2001:1::101/96 - IPV6INIT=yes -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -2469,7 +2445,6 @@ iface bond0 inet6 static +@@ -2591,7 +2569,6 @@ iface bond0 inet6 static HWADDR=52:54:00:12:34:00 IPADDR=192.168.1.2 NETMASK=255.255.255.0 @@ -233,7 +217,7 @@ index 54cc846..9985a97 100644 ONBOOT=no TYPE=Ethernet USERCTL=no -@@ -2479,7 +2454,6 @@ iface bond0 inet6 static +@@ -2601,7 +2578,6 @@ iface bond0 inet6 static DEVICE=eth1 HWADDR=52:54:00:12:34:aa MTU=1480 @@ -241,7 +225,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -2488,7 +2462,6 @@ iface bond0 inet6 static +@@ -2610,7 +2586,6 @@ iface bond0 inet6 static BOOTPROTO=none DEVICE=eth2 HWADDR=52:54:00:12:34:ff @@ -249,7 +233,7 @@ index 54cc846..9985a97 100644 ONBOOT=no TYPE=Ethernet USERCTL=no -@@ -2905,7 +2878,6 @@ class TestRhelSysConfigRendering(CiTestCase): +@@ -3027,7 +3002,6 @@ class TestRhelSysConfigRendering(CiTestCase): BOOTPROTO=dhcp DEVICE=eth1000 HWADDR=07-1c-c6-75-a4-be @@ -257,7 +241,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -3026,7 +2998,6 @@ GATEWAY=10.0.2.2 +@@ -3148,7 +3122,6 @@ GATEWAY=10.0.2.2 HWADDR=52:54:00:12:34:00 IPADDR=10.0.2.15 NETMASK=255.255.255.0 @@ -265,7 +249,7 @@ index 54cc846..9985a97 100644 ONBOOT=yes TYPE=Ethernet USERCTL=no -@@ -3096,7 +3067,6 @@ USERCTL=no +@@ -3218,7 +3191,6 @@ USERCTL=no # BOOTPROTO=dhcp DEVICE=eth0 @@ -274,5 +258,5 @@ index 54cc846..9985a97 100644 TYPE=Ethernet USERCTL=no -- -1.8.3.1 +2.27.0 diff --git a/SOURCES/0003-limit-permissions-on-def_log_file.patch b/SOURCES/0003-limit-permissions-on-def_log_file.patch index 7ec19f6..941adaf 100644 --- a/SOURCES/0003-limit-permissions-on-def_log_file.patch +++ b/SOURCES/0003-limit-permissions-on-def_log_file.patch @@ -1,6 +1,6 @@ -From de22eafc9046b8ea6fddda7440df5a05f5a40607 Mon Sep 17 00:00:00 2001 +From 6134624f10ef56534e37624adc12f11b09910591 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:49:53 +0200 +Date: Fri, 7 May 2021 13:36:08 +0200 Subject: limit permissions on def_log_file This sets a default mode of 0600 on def_log_file, and makes this @@ -10,18 +10,22 @@ LP: #1541196 Resolves: rhbz#1424612 X-approved-upstream: true +Conflicts 21.1: + cloudinit/stages.py: adjusting call of ensure_file() to use more +recent version + Signed-off-by: Eduardo Otubo --- cloudinit/settings.py | 1 + - cloudinit/stages.py | 3 ++- + cloudinit/stages.py | 1 + doc/examples/cloud-config.txt | 4 ++++ - 3 files changed, 7 insertions(+), 1 deletion(-) + 3 files changed, 6 insertions(+) diff --git a/cloudinit/settings.py b/cloudinit/settings.py -index 3a04a58..439eee0 100644 +index e690c0fd..43a1490c 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py -@@ -45,6 +45,7 @@ CFG_BUILTIN = { +@@ -46,6 +46,7 @@ CFG_BUILTIN = { 'None', ], 'def_log_file': '/var/log/cloud-init.log', @@ -30,22 +34,19 @@ index 3a04a58..439eee0 100644 'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'], 'ssh_deletekeys': False, diff --git a/cloudinit/stages.py b/cloudinit/stages.py -index 765f4aa..d769375 100644 +index 3ef4491c..83e25dd1 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py -@@ -147,8 +147,9 @@ class Init(object): +@@ -147,6 +147,7 @@ class Init(object): def _initialize_filesystem(self): util.ensure_dirs(self._initial_subdirs()) log_file = util.get_cfg_option_str(self.cfg, 'def_log_file') + log_file_mode = util.get_cfg_option_int(self.cfg, 'def_log_file_mode') if log_file: -- util.ensure_file(log_file) -+ util.ensure_file(log_file, mode=log_file_mode) + util.ensure_file(log_file, preserve_mode=True) perms = self.cfg.get('syslog_fix_perms') - if not perms: - perms = {} diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt -index f3ae5e6..b5b1fdd 100644 +index de9a0f87..bb33ad45 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -414,10 +414,14 @@ timezone: US/Eastern @@ -64,5 +65,5 @@ index f3ae5e6..b5b1fdd 100644 # you can set passwords for a user or multiple users -- -1.8.3.1 +2.27.0 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 index ad8c142..4d5a0d2 100644 --- 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 @@ -1,6 +1,6 @@ -From bb87d9a83ddbc5bf84fbdab9c58dedc0c9629eea Mon Sep 17 00:00:00 2001 +From 699d37a6ff3e343e214943794aac09e4156c2b2b Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:51:34 +0200 +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 @@ -20,10 +20,10 @@ Signed-off-by: Eduardo Otubo 1 file changed, 1 insertion(+) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index 9985a97..2cc57fe 100644 +index c67b5fcc..4ea0e597 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py -@@ -1614,6 +1614,7 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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 @@ -32,5 +32,5 @@ index 9985a97..2cc57fe 100644 MACADDR=aa:bb:cc:dd:ee:ff ONBOOT=yes -- -1.8.3.1 +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 index 08474eb..100d3a2 100644 --- a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch +++ b/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch @@ -1,6 +1,6 @@ -From 9c6562c6d3516df8d11aa7cf7cd9cc62e5c91a70 Mon Sep 17 00:00:00 2001 +From ccc75c1be3ae08d813193071c798fc905b5c03e5 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:51:37 +0200 +Date: Fri, 7 May 2021 13:36:12 +0200 Subject: DataSourceAzure.py: use hostnamectl to set hostname RH-Author: Vitaly Kuznetsov @@ -40,10 +40,10 @@ Signed-off-by: Miroslav Rezanina 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index f3c6452..1c214db 100755 +index cee630f7..553b5a7e 100755 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py -@@ -258,7 +258,7 @@ def get_hostname(hostname_command='hostname'): +@@ -296,7 +296,7 @@ def get_hostname(hostname_command='hostname'): def set_hostname(hostname, hostname_command='hostname'): @@ -53,5 +53,5 @@ index f3c6452..1c214db 100755 @azure_ds_telemetry_reporter -- -1.8.3.1 +2.27.0 diff --git a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch b/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch index 02058ba..6276255 100644 --- a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch +++ b/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch @@ -1,6 +1,6 @@ -From bdcad981ac530277529d1c77fb5e9e6f89409bd8 Mon Sep 17 00:00:00 2001 +From dfea0490b899804761fbd7aa23822783d7c36ec5 Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:51:44 +0200 +Date: Fri, 7 May 2021 13:36:13 +0200 Subject: include 'NOZEROCONF=yes' in /etc/sysconfig/network RH-Author: Eduardo Otubo @@ -27,10 +27,10 @@ Signed-off-by: Miroslav Rezanina 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 23e467d..af093dd 100644 +index 3d276666..d5440998 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py -@@ -888,7 +888,16 @@ class Renderer(renderer.Renderer): +@@ -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)) @@ -49,10 +49,10 @@ index 23e467d..af093dd 100644 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 2cc57fe..9985a97 100644 +index 4ea0e597..c67b5fcc 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py -@@ -1614,7 +1614,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true +@@ -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 @@ -61,5 +61,5 @@ index 2cc57fe..9985a97 100644 MACADDR=aa:bb:cc:dd:ee:ff ONBOOT=yes -- -1.8.3.1 +2.27.0 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 index 816a799..9c9e4cc 100644 --- a/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch +++ b/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch @@ -1,6 +1,6 @@ -From a52c7b659c6569c78aad4b92303f289009da476c Mon Sep 17 00:00:00 2001 +From 24894dcf45a307f44e29dc5d5b2d864b75fd982c Mon Sep 17 00:00:00 2001 From: Eduardo Otubo -Date: Mon, 5 Oct 2020 13:51:50 +0200 +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> @@ -114,13 +114,12 @@ Date: Thu May 28 08:44:06 2020 +0200 Signed-off-by: Eduardo Otubo Signed-off-by: Miroslav Rezanina --- - rhel/cloud.cfg | 2 +- - rhel/systemd/cloud-final.service | 2 ++ - rhel/systemd/cloud-init.service | 1 + - 3 files changed, 4 insertions(+), 1 deletion(-) + 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 82e8bf6..9ecba21 100644 +index 82e8bf62..9ecba215 100644 --- a/rhel/cloud.cfg +++ b/rhel/cloud.cfg @@ -6,7 +6,7 @@ ssh_pwauth: 0 @@ -132,21 +131,8 @@ index 82e8bf6..9ecba21 100644 ssh_genkeytypes: ~ syslog_fix_perms: ~ disable_vmware_customization: false -diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service -index 739b7e3..05add07 100644 ---- a/rhel/systemd/cloud-final.service -+++ b/rhel/systemd/cloud-final.service -@@ -11,6 +11,8 @@ ExecStart=/usr/bin/cloud-init modules --mode=final - RemainAfterExit=yes - TimeoutSec=0 - KillMode=process -+ExecStartPost=/bin/echo "trying to reload or restart NetworkManager.service" -+ExecStartPost=/usr/bin/systemctl try-reload-or-restart NetworkManager.service - - # Output needs to appear in instance console output - StandardOutput=journal+console diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service -index d0023a0..0b3d796 100644 +index d0023a05..0b3d796d 100644 --- a/rhel/systemd/cloud-init.service +++ b/rhel/systemd/cloud-init.service @@ -5,6 +5,7 @@ Wants=sshd-keygen.service @@ -158,5 +144,5 @@ index d0023a0..0b3d796 100644 Before=sshd-keygen.service Before=sshd.service -- -1.8.3.1 +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 new file mode 100644 index 0000000..38f08cc --- /dev/null +++ b/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch @@ -0,0 +1,496 @@ +From b48dda73da94782d7ab0c455fa382d3a5ef3c419 Mon Sep 17 00:00:00 2001 +From: Daniel Watkins +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 new file mode 100644 index 0000000..0d474bc --- /dev/null +++ b/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch @@ -0,0 +1,87 @@ +From bec5fb60ffae3d1137c7261e5571c2751c5dda25 Mon Sep 17 00:00:00 2001 +From: James Falcon +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-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch b/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch deleted file mode 100644 index c3f45ff..0000000 --- a/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch +++ /dev/null @@ -1,496 +0,0 @@ -From c3a1b3a5d7abe51a1facbdae71aca4b2bca7d6aa Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Wed, 28 Oct 2020 20:43:33 +0100 -Subject: [PATCH 2/3] Add config modules for controlling IBM PowerVM RMC. - (#584) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 12: Support for cloud-init config modules for PowerVM Hypervisor in Red Hat cloud-init -RH-Commit: [1/1] d175c3607a8d4f473573ba0ce42e0f311dbc31ed (eterrell/cloud-init) -RH-Bugzilla: 1886430 - -commit f99d4f96b00a9cfec1c721d364cbfd728674e5dc (upstream/master) -Author: Aman306 <45781773+Aman306@users.noreply.github.com> -Date: Wed Oct 28 23:36:09 2020 +0530 - - Add config modules for controlling IBM PowerVM RMC. (#584) - - Reliable Scalable Cluster Technology (RSCT) is a set of software - components that together provide a comprehensive clustering - environment(RAS features) for IBM PowerVM based virtual machines. RSCT - includes the Resource Monitoring and Control (RMC) subsystem. RMC is a - generalized framework used for managing, monitoring, and manipulating - resources. RMC runs as a daemon process on individual machines and needs - creation of unique node id and restarts during VM boot. - - LP: #1895979 - - Co-authored-by: Scott Moser - -Signed-off-by: Eduardo Otubo ---- - cloudinit/config/cc_refresh_rmc_and_interface.py | 159 +++++++++++++++++++++ - cloudinit/config/cc_reset_rmc.py | 143 ++++++++++++++++++ - config/cloud.cfg.tmpl | 2 + - .../test_handler_refresh_rmc_and_interface.py | 109 ++++++++++++++ - tools/.github-cla-signers | 1 + - 5 files changed, 414 insertions(+) - create mode 100644 cloudinit/config/cc_refresh_rmc_and_interface.py - create mode 100644 cloudinit/config/cc_reset_rmc.py - create mode 100644 tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py - -diff --git a/cloudinit/config/cc_refresh_rmc_and_interface.py b/cloudinit/config/cc_refresh_rmc_and_interface.py -new file mode 100644 -index 0000000..146758a ---- /dev/null -+++ b/cloudinit/config/cc_refresh_rmc_and_interface.py -@@ -0,0 +1,159 @@ -+# (c) Copyright IBM Corp. 2020 All Rights Reserved -+# -+# Author: Aman Kumar Sinha -+# -+# This file is part of cloud-init. See LICENSE file for license information. -+ -+""" -+Refresh IPv6 interface and RMC -+------------------------------ -+**Summary:** Ensure Network Manager is not managing IPv6 interface -+ -+This module is IBM PowerVM Hypervisor specific -+ -+Reliable Scalable Cluster Technology (RSCT) is a set of software components -+that together provide a comprehensive clustering environment(RAS features) -+for IBM PowerVM based virtual machines. RSCT includes the Resource -+Monitoring and Control (RMC) subsystem. RMC is a generalized framework used -+for managing, monitoring, and manipulating resources. RMC runs as a daemon -+process on individual machines and needs creation of unique node id and -+restarts during VM boot. -+More details refer -+https://www.ibm.com/support/knowledgecenter/en/SGVKBA_3.2/admin/bl503_ovrv.htm -+ -+This module handles -+- Refreshing RMC -+- Disabling NetworkManager from handling IPv6 interface, as IPv6 interface -+ is used for communication between RMC daemon and PowerVM hypervisor. -+ -+**Internal name:** ``cc_refresh_rmc_and_interface`` -+ -+**Module frequency:** per always -+ -+**Supported distros:** RHEL -+ -+""" -+ -+from cloudinit import log as logging -+from cloudinit.settings import PER_ALWAYS -+from cloudinit import util -+from cloudinit import subp -+from cloudinit import netinfo -+ -+import errno -+ -+frequency = PER_ALWAYS -+ -+LOG = logging.getLogger(__name__) -+# Ensure that /opt/rsct/bin has been added to standard PATH of the -+# distro. The symlink to rmcctrl is /usr/sbin/rsct/bin/rmcctrl . -+RMCCTRL = 'rmcctrl' -+ -+ -+def handle(name, _cfg, _cloud, _log, _args): -+ if not subp.which(RMCCTRL): -+ LOG.debug("No '%s' in path, disabled", RMCCTRL) -+ return -+ -+ LOG.debug( -+ 'Making the IPv6 up explicitly. ' -+ 'Ensuring IPv6 interface is not being handled by NetworkManager ' -+ 'and it is restarted to re-establish the communication with ' -+ 'the hypervisor') -+ -+ ifaces = find_ipv6_ifaces() -+ -+ # Setting NM_CONTROLLED=no for IPv6 interface -+ # making it down and up -+ -+ if len(ifaces) == 0: -+ LOG.debug("Did not find any interfaces with ipv6 addresses.") -+ else: -+ for iface in ifaces: -+ refresh_ipv6(iface) -+ disable_ipv6(sysconfig_path(iface)) -+ restart_network_manager() -+ -+ -+def find_ipv6_ifaces(): -+ info = netinfo.netdev_info() -+ ifaces = [] -+ for iface, data in info.items(): -+ if iface == "lo": -+ LOG.debug('Skipping localhost interface') -+ if len(data.get("ipv4", [])) != 0: -+ # skip this interface, as it has ipv4 addrs -+ continue -+ ifaces.append(iface) -+ return ifaces -+ -+ -+def refresh_ipv6(interface): -+ # IPv6 interface is explicitly brought up, subsequent to which the -+ # RMC services are restarted to re-establish the communication with -+ # the hypervisor. -+ subp.subp(['ip', 'link', 'set', interface, 'down']) -+ subp.subp(['ip', 'link', 'set', interface, 'up']) -+ -+ -+def sysconfig_path(iface): -+ return '/etc/sysconfig/network-scripts/ifcfg-' + iface -+ -+ -+def restart_network_manager(): -+ subp.subp(['systemctl', 'restart', 'NetworkManager']) -+ -+ -+def disable_ipv6(iface_file): -+ # Ensuring that the communication b/w the hypervisor and VM is not -+ # interrupted due to NetworkManager. For this purpose, as part of -+ # this function, the NM_CONTROLLED is explicitly set to No for IPV6 -+ # interface and NetworkManager is restarted. -+ try: -+ contents = util.load_file(iface_file) -+ except IOError as e: -+ if e.errno == errno.ENOENT: -+ LOG.debug("IPv6 interface file %s does not exist\n", -+ iface_file) -+ else: -+ raise e -+ -+ if 'IPV6INIT' not in contents: -+ LOG.debug("Interface file %s did not have IPV6INIT", iface_file) -+ return -+ -+ LOG.debug("Editing interface file %s ", iface_file) -+ -+ # Dropping any NM_CONTROLLED or IPV6 lines from IPv6 interface file. -+ lines = contents.splitlines() -+ lines = [line for line in lines if not search(line)] -+ lines.append("NM_CONTROLLED=no") -+ -+ with open(iface_file, "w") as fp: -+ fp.write("\n".join(lines) + "\n") -+ -+ -+def search(contents): -+ # Search for any NM_CONTROLLED or IPV6 lines in IPv6 interface file. -+ return( -+ contents.startswith("IPV6ADDR") or -+ contents.startswith("IPADDR6") or -+ contents.startswith("IPV6INIT") or -+ contents.startswith("NM_CONTROLLED")) -+ -+ -+def refresh_rmc(): -+ # To make a healthy connection between RMC daemon and hypervisor we -+ # refresh RMC. With refreshing RMC we are ensuring that making IPv6 -+ # down and up shouldn't impact communication between RMC daemon and -+ # hypervisor. -+ # -z : stop Resource Monitoring & Control subsystem and all resource -+ # managers, but the command does not return control to the user -+ # until the subsystem and all resource managers are stopped. -+ # -s : start Resource Monitoring & Control subsystem. -+ try: -+ subp.subp([RMCCTRL, '-z']) -+ subp.subp([RMCCTRL, '-s']) -+ except Exception: -+ util.logexc(LOG, 'Failed to refresh the RMC subsystem.') -+ raise -diff --git a/cloudinit/config/cc_reset_rmc.py b/cloudinit/config/cc_reset_rmc.py -new file mode 100644 -index 0000000..1cd7277 ---- /dev/null -+++ b/cloudinit/config/cc_reset_rmc.py -@@ -0,0 +1,143 @@ -+# (c) Copyright IBM Corp. 2020 All Rights Reserved -+# -+# Author: Aman Kumar Sinha -+# -+# This file is part of cloud-init. See LICENSE file for license information. -+ -+ -+""" -+Reset RMC -+------------ -+**Summary:** reset rsct node id -+ -+Reset RMC module is IBM PowerVM Hypervisor specific -+ -+Reliable Scalable Cluster Technology (RSCT) is a set of software components, -+that together provide a comprehensive clustering environment (RAS features) -+for IBM PowerVM based virtual machines. RSCT includes the Resource monitoring -+and control (RMC) subsystem. RMC is a generalized framework used for managing, -+monitoring, and manipulating resources. RMC runs as a daemon process on -+individual machines and needs creation of unique node id and restarts -+during VM boot. -+More details refer -+https://www.ibm.com/support/knowledgecenter/en/SGVKBA_3.2/admin/bl503_ovrv.htm -+ -+This module handles -+- creation of the unique RSCT node id to every instance/virtual machine -+ and ensure once set, it isn't changed subsequently by cloud-init. -+ In order to do so, it restarts RSCT service. -+ -+Prerequisite of using this module is to install RSCT packages. -+ -+**Internal name:** ``cc_reset_rmc`` -+ -+**Module frequency:** per instance -+ -+**Supported distros:** rhel, sles and ubuntu -+ -+""" -+import os -+ -+from cloudinit import log as logging -+from cloudinit.settings import PER_INSTANCE -+from cloudinit import util -+from cloudinit import subp -+ -+frequency = PER_INSTANCE -+ -+# RMCCTRL is expected to be in system PATH (/opt/rsct/bin) -+# The symlink for RMCCTRL and RECFGCT are -+# /usr/sbin/rsct/bin/rmcctrl and -+# /usr/sbin/rsct/install/bin/recfgct respectively. -+RSCT_PATH = '/opt/rsct/install/bin' -+RMCCTRL = 'rmcctrl' -+RECFGCT = 'recfgct' -+ -+LOG = logging.getLogger(__name__) -+ -+NODE_ID_FILE = '/etc/ct_node_id' -+ -+ -+def handle(name, _cfg, cloud, _log, _args): -+ # Ensuring node id has to be generated only once during first boot -+ if cloud.datasource.platform_type == 'none': -+ LOG.debug('Skipping creation of new ct_node_id node') -+ return -+ -+ if not os.path.isdir(RSCT_PATH): -+ LOG.debug("module disabled, RSCT_PATH not present") -+ return -+ -+ orig_path = os.environ.get('PATH') -+ try: -+ add_path(orig_path) -+ reset_rmc() -+ finally: -+ if orig_path: -+ os.environ['PATH'] = orig_path -+ else: -+ del os.environ['PATH'] -+ -+ -+def reconfigure_rsct_subsystems(): -+ # Reconfigure the RSCT subsystems, which includes removing all RSCT data -+ # under the /var/ct directory, generating a new node ID, and making it -+ # appear as if the RSCT components were just installed -+ try: -+ out = subp.subp([RECFGCT])[0] -+ LOG.debug(out.strip()) -+ return out -+ except subp.ProcessExecutionError: -+ util.logexc(LOG, 'Failed to reconfigure the RSCT subsystems.') -+ raise -+ -+ -+def get_node_id(): -+ try: -+ fp = util.load_file(NODE_ID_FILE) -+ node_id = fp.split('\n')[0] -+ return node_id -+ except Exception: -+ util.logexc(LOG, 'Failed to get node ID from file %s.' % NODE_ID_FILE) -+ raise -+ -+ -+def add_path(orig_path): -+ # Adding the RSCT_PATH to env standard path -+ # So thet cloud init automatically find and -+ # run RECFGCT to create new node_id. -+ suff = ":" + orig_path if orig_path else "" -+ os.environ['PATH'] = RSCT_PATH + suff -+ return os.environ['PATH'] -+ -+ -+def rmcctrl(): -+ # Stop the RMC subsystem and all resource managers so that we can make -+ # some changes to it -+ try: -+ return subp.subp([RMCCTRL, '-z']) -+ except Exception: -+ util.logexc(LOG, 'Failed to stop the RMC subsystem.') -+ raise -+ -+ -+def reset_rmc(): -+ LOG.debug('Attempting to reset RMC.') -+ -+ node_id_before = get_node_id() -+ LOG.debug('Node ID at beginning of module: %s', node_id_before) -+ -+ # Stop the RMC subsystem and all resource managers so that we can make -+ # some changes to it -+ rmcctrl() -+ reconfigure_rsct_subsystems() -+ -+ node_id_after = get_node_id() -+ LOG.debug('Node ID at end of module: %s', node_id_after) -+ -+ # Check if new node ID is generated or not -+ # by comparing old and new node ID -+ if node_id_after == node_id_before: -+ msg = 'New node ID did not get generated.' -+ LOG.error(msg) -+ raise Exception(msg) -diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl -index 2beb9b0..7171aaa 100644 ---- a/config/cloud.cfg.tmpl -+++ b/config/cloud.cfg.tmpl -@@ -135,6 +135,8 @@ cloud_final_modules: - - chef - - mcollective - - salt-minion -+ - reset_rmc -+ - refresh_rmc_and_interface - - rightscale_userdata - - scripts-vendor - - scripts-per-once -diff --git a/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py b/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py -new file mode 100644 -index 0000000..e13b779 ---- /dev/null -+++ b/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py -@@ -0,0 +1,109 @@ -+from cloudinit.config import cc_refresh_rmc_and_interface as ccrmci -+ -+from cloudinit import util -+ -+from cloudinit.tests import helpers as t_help -+from cloudinit.tests.helpers import mock -+ -+from textwrap import dedent -+import logging -+ -+LOG = logging.getLogger(__name__) -+MPATH = "cloudinit.config.cc_refresh_rmc_and_interface" -+NET_INFO = { -+ 'lo': {'ipv4': [{'ip': '127.0.0.1', -+ 'bcast': '', 'mask': '255.0.0.0', -+ 'scope': 'host'}], -+ 'ipv6': [{'ip': '::1/128', -+ 'scope6': 'host'}], 'hwaddr': '', -+ 'up': 'True'}, -+ 'env2': {'ipv4': [{'ip': '8.0.0.19', -+ 'bcast': '8.0.0.255', 'mask': '255.255.255.0', -+ 'scope': 'global'}], -+ 'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8220/64', -+ 'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:20', -+ 'up': 'True'}, -+ 'env3': {'ipv4': [{'ip': '90.0.0.14', -+ 'bcast': '90.0.0.255', 'mask': '255.255.255.0', -+ 'scope': 'global'}], -+ 'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8221/64', -+ 'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:21', -+ 'up': 'True'}, -+ 'env4': {'ipv4': [{'ip': '9.114.23.7', -+ 'bcast': '9.114.23.255', 'mask': '255.255.255.0', -+ 'scope': 'global'}], -+ 'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8222/64', -+ 'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:22', -+ 'up': 'True'}, -+ 'env5': {'ipv4': [], -+ 'ipv6': [{'ip': 'fe80::9c26:c3ff:fea4:62c8/64', -+ 'scope6': 'link'}], 'hwaddr': '42:20:86:df:fa:4c', -+ 'up': 'True'}} -+ -+ -+class TestRsctNodeFile(t_help.CiTestCase): -+ def test_disable_ipv6_interface(self): -+ """test parsing of iface files.""" -+ fname = self.tmp_path("iface-eth5") -+ util.write_file(fname, dedent("""\ -+ BOOTPROTO=static -+ DEVICE=eth5 -+ HWADDR=42:20:86:df:fa:4c -+ IPV6INIT=yes -+ IPADDR6=fe80::9c26:c3ff:fea4:62c8/64 -+ IPV6ADDR=fe80::9c26:c3ff:fea4:62c8/64 -+ NM_CONTROLLED=yes -+ ONBOOT=yes -+ STARTMODE=auto -+ TYPE=Ethernet -+ USERCTL=no -+ """)) -+ -+ ccrmci.disable_ipv6(fname) -+ self.assertEqual(dedent("""\ -+ BOOTPROTO=static -+ DEVICE=eth5 -+ HWADDR=42:20:86:df:fa:4c -+ ONBOOT=yes -+ STARTMODE=auto -+ TYPE=Ethernet -+ USERCTL=no -+ NM_CONTROLLED=no -+ """), util.load_file(fname)) -+ -+ @mock.patch(MPATH + '.refresh_rmc') -+ @mock.patch(MPATH + '.restart_network_manager') -+ @mock.patch(MPATH + '.disable_ipv6') -+ @mock.patch(MPATH + '.refresh_ipv6') -+ @mock.patch(MPATH + '.netinfo.netdev_info') -+ @mock.patch(MPATH + '.subp.which') -+ def test_handle(self, m_refresh_rmc, -+ m_netdev_info, m_refresh_ipv6, m_disable_ipv6, -+ m_restart_nm, m_which): -+ """Basic test of handle.""" -+ m_netdev_info.return_value = NET_INFO -+ m_which.return_value = '/opt/rsct/bin/rmcctrl' -+ ccrmci.handle( -+ "refresh_rmc_and_interface", None, None, None, None) -+ self.assertEqual(1, m_netdev_info.call_count) -+ m_refresh_ipv6.assert_called_with('env5') -+ m_disable_ipv6.assert_called_with( -+ '/etc/sysconfig/network-scripts/ifcfg-env5') -+ self.assertEqual(1, m_restart_nm.call_count) -+ self.assertEqual(1, m_refresh_rmc.call_count) -+ -+ @mock.patch(MPATH + '.netinfo.netdev_info') -+ def test_find_ipv6(self, m_netdev_info): -+ """find_ipv6_ifaces parses netdev_info returning those with ipv6""" -+ m_netdev_info.return_value = NET_INFO -+ found = ccrmci.find_ipv6_ifaces() -+ self.assertEqual(['env5'], found) -+ -+ @mock.patch(MPATH + '.subp.subp') -+ def test_refresh_ipv6(self, m_subp): -+ """refresh_ipv6 should ip down and up the interface.""" -+ iface = "myeth0" -+ ccrmci.refresh_ipv6(iface) -+ m_subp.assert_has_calls([ -+ mock.call(['ip', 'link', 'set', iface, 'down']), -+ mock.call(['ip', 'link', 'set', iface, 'up'])]) -diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers -index c67db43..802a35b 100644 ---- a/tools/.github-cla-signers -+++ b/tools/.github-cla-signers -@@ -1,4 +1,5 @@ - AlexBaranowski -+Aman306 - beezly - bipinbachhao - BirknerAlex --- -1.8.3.1 - diff --git a/SOURCES/ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch b/SOURCES/ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch deleted file mode 100644 index c31b4b2..0000000 --- a/SOURCES/ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch +++ /dev/null @@ -1,58 +0,0 @@ -From 8a7d21fa739901bad847294004266dba76c027af Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Tue, 1 Dec 2020 15:51:47 +0100 -Subject: [PATCH 2/4] Adding BOOTPROTO = dhcp to render sysconfig dhcp6 - stateful on RHEL (#685) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 25: Adding BOOTPROTO = dhcp to render sysconfig dhcp6 stateful on RHEL (#685) -RH-Commit: [1/1] b7304323096b1e40287950e44cf7aa3cdb4ba99e (eterrell/cloud-init) -RH-Bugzilla: 1859695 - -BOOTPROTO needs to be set to 'dhcp' on RHEL so NetworkManager can -properly acquire ipv6 address. - -rhbz: #1859695 - -Signed-off-by: Eduardo Otubo - -Co-authored-by: Daniel Watkins -Co-authored-by: Scott Moser ---- - cloudinit/net/sysconfig.py | 6 ++++++ - tests/unittests/test_net.py | 2 +- - 2 files changed, 7 insertions(+), 1 deletion(-) - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 078636a4..94801a93 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -391,6 +391,12 @@ class Renderer(renderer.Renderer): - # Only IPv6 is DHCP, IPv4 may be static - iface_cfg['BOOTPROTO'] = 'dhcp6' - iface_cfg['DHCLIENT6_MODE'] = 'managed' -+ # only if rhel AND dhcpv6 stateful -+ elif (flavor == 'rhel' and -+ subnet_type == 'ipv6_dhcpv6-stateful'): -+ iface_cfg['BOOTPROTO'] = 'dhcp' -+ iface_cfg['DHCPV6C'] = True -+ iface_cfg['IPV6INIT'] = True - else: - iface_cfg['IPV6INIT'] = True - # Configure network settings using DHCPv6 -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index c0337459..bcd261db 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -1359,7 +1359,7 @@ NETWORK_CONFIGS = { - }, - 'expected_sysconfig_rhel': { - 'ifcfg-iface0': textwrap.dedent("""\ -- BOOTPROTO=none -+ BOOTPROTO=dhcp - DEVICE=iface0 - DHCPV6C=yes - IPV6INIT=yes --- -2.18.4 - diff --git a/SOURCES/ci-DataSourceAzure-update-password-for-defuser-if-exist.patch b/SOURCES/ci-DataSourceAzure-update-password-for-defuser-if-exist.patch deleted file mode 100644 index 7a9f478..0000000 --- a/SOURCES/ci-DataSourceAzure-update-password-for-defuser-if-exist.patch +++ /dev/null @@ -1,60 +0,0 @@ -From bcbd6be99d8317793aff905c4222c351a1bf5c46 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 21 Jan 2021 10:08:49 +0100 -Subject: [PATCH 1/2] DataSourceAzure: update password for defuser if exists - (#671) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 37: DataSourceAzure: update password for defuser if exists (#671) -RH-Commit: [1/1] 264092a68a3771cc4ed99dad5b93f7a1433e143a (eterrell/cloud-init) -RH-Bugzilla: 1900892 - -commit eea754492f074e00b601cf77aa278e3623857c5a -Author: Anh Vo -Date: Thu Nov 19 00:35:46 2020 -0500 - - DataSourceAzure: update password for defuser if exists (#671) - - cc_set_password will only update the password for the default user if - cfg['password'] is set. The existing code of datasource Azure will fail - to update the default user's password because it does not set that - metadata. If the default user doesn't exist in the image, the current - code works fine because the password is set during user create and - not in cc_set_password - -Signed-off-by: Eduardo Otubo ---- - cloudinit/sources/DataSourceAzure.py | 2 +- - tests/unittests/test_datasource/test_azure.py | 3 +++ - 2 files changed, 4 insertions(+), 1 deletion(-) - -diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py -index 1c214db9..d4a2d60f 100755 ---- a/cloudinit/sources/DataSourceAzure.py -+++ b/cloudinit/sources/DataSourceAzure.py -@@ -1231,7 +1231,7 @@ def read_azure_ovf(contents): - if password: - defuser['lock_passwd'] = False - if DEF_PASSWD_REDACTION != password: -- defuser['passwd'] = encrypt_pass(password) -+ defuser['passwd'] = cfg['password'] = encrypt_pass(password) - - if defuser: - cfg['system_info'] = {'default_user': defuser} -diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py -index 47e03bd1..2059990a 100644 ---- a/tests/unittests/test_datasource/test_azure.py -+++ b/tests/unittests/test_datasource/test_azure.py -@@ -919,6 +919,9 @@ scbus-1 on xpt0 bus 0 - crypt.crypt(odata['UserPassword'], - defuser['passwd'][0:pos])) - -+ # the same hashed value should also be present in cfg['password'] -+ self.assertEqual(defuser['passwd'], dsrc.cfg['password']) -+ - def test_user_not_locked_if_password_redacted(self): - odata = {'HostName': "myhost", 'UserName': "myuser", - 'UserPassword': dsaz.DEF_PASSWD_REDACTION} --- -2.18.4 - diff --git a/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch b/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch deleted file mode 100644 index a0d9156..0000000 --- a/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch +++ /dev/null @@ -1,295 +0,0 @@ -From 5ded09d5acf4d653fe2cbd54814f53063d265489 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 29 Oct 2020 15:05:42 +0100 -Subject: [PATCH 1/3] Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on - static6 (#634) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 13: [RHEL-8.4.0] Add support for ipv6_autoconf on cloud-init-20.3 -RH-Commit: [1/1] 41e61c35893f4487981a1ad31f9f97a9a740b397 (eterrell/cloud-init) -RH-Bugzilla: 1889635 - -commit b46e4a8cff667c8441622089cf7d57aeb88220cd -Author: Eduardo Otubo -Date: Thu Oct 29 15:05:42 2020 +0100 - - Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on static6 (#634) - - The static and static6 subnet types for network_data.json were - being ignored by the Openstack handler, this would cause the code to - break and not function properly. - - As of today, if a static6 configuration is chosen, the interface will - still eventually be available to receive router advertisements or be set - from NetworkManager to wait for them and cycle the interface in negative - case. - - It is safe to assume that if the interface is manually configured to use - static ipv6 address, there's no need to wait for router advertisements. - This patch will set automatically IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA - both to "no" in this case. - - This patch fixes the specific behavior only for RHEL flavor and - sysconfig renderer. It also introduces new unit tests for the specific - case as well as adjusts some existent tests to be compatible with the - new options. This patch also addresses this problem by assigning the - appropriate subnet type for each case on the openstack handler. - - rhbz: #1889635 - rhbz: #1889635 - - Signed-off-by: Eduardo Otubo otubo@redhat.com - -Signed-off-by: Eduardo Otubo otubo@redhat.com ---- - cloudinit/net/network_state.py | 3 +- - cloudinit/net/sysconfig.py | 4 + - cloudinit/sources/helpers/openstack.py | 8 +- - tests/unittests/test_distros/test_netconfig.py | 2 + - tests/unittests/test_net.py | 100 +++++++++++++++++++++++++ - 5 files changed, 115 insertions(+), 2 deletions(-) - -diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py -index b2f7d31..d9e7fd5 100644 ---- a/cloudinit/net/network_state.py -+++ b/cloudinit/net/network_state.py -@@ -820,7 +820,8 @@ def _normalize_subnet(subnet): - - if subnet.get('type') in ('static', 'static6'): - normal_subnet.update( -- _normalize_net_keys(normal_subnet, address_keys=('address',))) -+ _normalize_net_keys(normal_subnet, address_keys=( -+ 'address', 'ip_address',))) - normal_subnet['routes'] = [_normalize_route(r) - for r in subnet.get('routes', [])] - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index af093dd..c078898 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -451,6 +451,10 @@ class Renderer(renderer.Renderer): - iface_cfg[mtu_key] = subnet['mtu'] - else: - iface_cfg[mtu_key] = subnet['mtu'] -+ -+ if subnet_is_ipv6(subnet) and flavor == 'rhel': -+ iface_cfg['IPV6_FORCE_ACCEPT_RA'] = False -+ iface_cfg['IPV6_AUTOCONF'] = False - elif subnet_type == 'manual': - if flavor == 'suse': - LOG.debug('Unknown subnet type setting "%s"', subnet_type) -diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py -index 65e020c..3e6365f 100644 ---- a/cloudinit/sources/helpers/openstack.py -+++ b/cloudinit/sources/helpers/openstack.py -@@ -602,11 +602,17 @@ def convert_net_json(network_json=None, known_macs=None): - elif network['type'] in ['ipv6_slaac', 'ipv6_dhcpv6-stateless', - 'ipv6_dhcpv6-stateful']: - subnet.update({'type': network['type']}) -- elif network['type'] in ['ipv4', 'ipv6']: -+ elif network['type'] in ['ipv4', 'static']: - subnet.update({ - 'type': 'static', - 'address': network.get('ip_address'), - }) -+ elif network['type'] in ['ipv6', 'static6']: -+ cfg.update({'accept-ra': False}) -+ subnet.update({ -+ 'type': 'static6', -+ 'address': network.get('ip_address'), -+ }) - - # Enable accept_ra for stateful and legacy ipv6_dhcp types - if network['type'] in ['ipv6_dhcpv6-stateful', 'ipv6_dhcp']: -diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py -index 8d7b09c..f9fc3a1 100644 ---- a/tests/unittests/test_distros/test_netconfig.py -+++ b/tests/unittests/test_distros/test_netconfig.py -@@ -514,7 +514,9 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - DEVICE=eth0 - IPV6ADDR=2607:f0d0:1002:0011::2/64 - IPV6INIT=yes -+ IPV6_AUTOCONF=no - IPV6_DEFAULTGW=2607:f0d0:1002:0011::1 -+ IPV6_FORCE_ACCEPT_RA=no - NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index 9985a97..d7a7a65 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -750,7 +750,9 @@ IPADDR=172.19.1.34 - IPV6ADDR=2001:DB8::10/64 - IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64" - IPV6INIT=yes -+IPV6_AUTOCONF=no - IPV6_DEFAULTGW=2001:DB8::1 -+IPV6_FORCE_ACCEPT_RA=no - NETMASK=255.255.252.0 - ONBOOT=yes - TYPE=Ethernet -@@ -1022,6 +1024,8 @@ NETWORK_CONFIGS = { - IPADDR=192.168.14.2 - IPV6ADDR=2001:1::1/64 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no - NETMASK=255.255.255.0 - ONBOOT=yes - TYPE=Ethernet -@@ -1247,6 +1251,33 @@ NETWORK_CONFIGS = { - """), - }, - }, -+ 'static6': { -+ 'yaml': textwrap.dedent("""\ -+ version: 1 -+ config: -+ - type: 'physical' -+ name: 'iface0' -+ accept-ra: 'no' -+ subnets: -+ - type: 'static6' -+ address: 2001:1::1/64 -+ """).rstrip(' '), -+ 'expected_sysconfig_rhel': { -+ 'ifcfg-iface0': textwrap.dedent("""\ -+ BOOTPROTO=none -+ DEVICE=iface0 -+ IPV6ADDR=2001:1::1/64 -+ IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no -+ DEVICE=iface0 -+ NM_CONTROLLED=no -+ ONBOOT=yes -+ TYPE=Ethernet -+ USERCTL=no -+ """), -+ }, -+ }, - 'dhcpv6_stateless': { - 'expected_eni': textwrap.dedent("""\ - auto lo -@@ -1636,6 +1667,8 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - IPADDR=192.168.14.2 - IPV6ADDR=2001:1::1/64 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no - IPV6_DEFAULTGW=2001:4800:78ff:1b::1 - MACADDR=bb:bb:bb:bb:bb:aa - NETMASK=255.255.255.0 -@@ -2158,6 +2191,8 @@ iface bond0 inet6 static - IPADDR1=192.168.1.2 - IPV6ADDR=2001:1::1/92 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no - MTU=9000 - NETMASK=255.255.255.0 - NETMASK1=255.255.255.0 -@@ -2259,6 +2294,8 @@ iface bond0 inet6 static - IPADDR1=192.168.1.2 - IPV6ADDR=2001:1::bbbb/96 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no - IPV6_DEFAULTGW=2001:1::1 - MTU=2222 - NETMASK=255.255.255.0 -@@ -2341,6 +2378,9 @@ iface bond0 inet6 static - HWADDR=52:54:00:12:34:00 - IPV6ADDR=2001:1::100/96 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no -+ NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -2352,6 +2392,9 @@ iface bond0 inet6 static - HWADDR=52:54:00:12:34:01 - IPV6ADDR=2001:1::101/96 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no -+ NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3151,6 +3194,61 @@ USERCTL=no - self._compare_files_to_expected(entry[self.expected_name], found) - self._assert_headers(found) - -+ def test_stattic6_from_json(self): -+ net_json = { -+ "services": [{"type": "dns", "address": "172.19.0.12"}], -+ "networks": [{ -+ "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4", -+ "type": "ipv4", "netmask": "255.255.252.0", -+ "link": "tap1a81968a-79", -+ "routes": [{ -+ "netmask": "0.0.0.0", -+ "network": "0.0.0.0", -+ "gateway": "172.19.3.254", -+ }, { -+ "netmask": "0.0.0.0", # A second default gateway -+ "network": "0.0.0.0", -+ "gateway": "172.20.3.254", -+ }], -+ "ip_address": "172.19.1.34", "id": "network0" -+ }, { -+ "network_id": "mgmt", -+ "netmask": "ffff:ffff:ffff:ffff::", -+ "link": "interface1", -+ "mode": "link-local", -+ "routes": [], -+ "ip_address": "fe80::c096:67ff:fe5c:6e84", -+ "type": "static6", -+ "id": "network1", -+ "services": [], -+ "accept-ra": "false" -+ }], -+ "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'} -+ render_dir = self.tmp_dir() -+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs) -+ ns = network_state.parse_net_config_data(network_cfg, -+ skip_broken=False) -+ renderer = self._get_renderer() -+ with self.assertRaises(ValueError): -+ renderer.render_network_state(ns, target=render_dir) -+ self.assertEqual([], os.listdir(render_dir)) -+ -+ def test_static6_from_yaml(self): -+ entry = NETWORK_CONFIGS['static6'] -+ found = self._render_and_read(network_config=yaml.load( -+ entry['yaml'])) -+ self._compare_files_to_expected(entry[self.expected_name], found) -+ self._assert_headers(found) -+ - def test_dhcpv6_reject_ra_config_v2(self): - entry = NETWORK_CONFIGS['dhcpv6_reject_ra'] - found = self._render_and_read(network_config=yaml.load( -@@ -3268,6 +3366,8 @@ USERCTL=no - IPADDR=192.168.42.100 - IPV6ADDR=2001:db8::100/32 - IPV6INIT=yes -+ IPV6_AUTOCONF=no -+ IPV6_FORCE_ACCEPT_RA=no - IPV6_DEFAULTGW=2001:db8::1 - NETMASK=255.255.255.0 - NM_CONTROLLED=no --- -1.8.3.1 - 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 new file mode 100644 index 0000000..6a9cfcc --- /dev/null +++ b/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch @@ -0,0 +1,262 @@ +From 71989367e7a634fdd2af8ef58473975e0ef60464 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +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 +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 +RH-Acked-by: Eduardo Otubo + +TESTED: By me and QA +BREW: 39178090 + +Fix home permissions modified by ssh module (SC-338) (#984) + +commit 7d3f5d750f6111c2716143364ea33486df67c927 +Author: James Falcon +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 +--- + 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-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch b/SOURCES/ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch deleted file mode 100644 index aeaa342..0000000 --- a/SOURCES/ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch +++ /dev/null @@ -1,61 +0,0 @@ -From d3889c4645a1319c3d677006164b618ee53f4c8b Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Mon, 7 Dec 2020 14:23:22 +0100 -Subject: [PATCH 3/4] Fix unit failure of cloud-final.service if NetworkManager - was not present. - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 27: Fix unit failure of cloud-final.service if NetworkManager was not present. -RH-Commit: [1/1] 3c65a2cca140fff48df1ef32919e3cb035506a2b (eterrell/cloud-init) -RH-Bugzilla: 1898943 - -cloud-final.service would fail if NetworkManager was not installed. - -journal -u cloud-final.service would show: - - cloud-init[5328]: Cloud-init v. 19.4 finished at ... - echo[5346]: try restart NetworkManager.service - systemctl[5349]: Failed to reload-or-try-restart - NetworkManager.service: Unit not found. - systemd[1]: cloud-final.service: control process exited, - code=exited status=5 - systemd[1]: Failed to start Execute cloud user/final scripts. - systemd[1]: Unit cloud-final.service entered failed state. - systemd[1]: cloud-final.service failed. - -The change here is to only attempt to restart NetworkManager if it is -present, and its SubState is 'running'. - -The multi-line shell in a systemd unit is less than ideal, but I'm not -aware of any other way of conditionally doing this. - -Note that both of 'try-reload-or-restart' and 'reload-or-try-restart' -will fail if the service is not present. So this would also affect rhel -8 systems that do not use NetworkManager. - -Signed-off-by: Eduardo Otubo ---- - rhel/systemd/cloud-final.service | 7 +++++-- - 1 file changed, 5 insertions(+), 2 deletions(-) - -diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service -index 05add077..e281c0cf 100644 ---- a/rhel/systemd/cloud-final.service -+++ b/rhel/systemd/cloud-final.service -@@ -11,8 +11,11 @@ ExecStart=/usr/bin/cloud-init modules --mode=final - RemainAfterExit=yes - TimeoutSec=0 - KillMode=process --ExecStartPost=/bin/echo "trying to reload or restart NetworkManager.service" --ExecStartPost=/usr/bin/systemctl try-reload-or-restart NetworkManager.service -+# 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 --- -2.18.4 - diff --git a/SOURCES/ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch b/SOURCES/ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch deleted file mode 100644 index 3860cd1..0000000 --- a/SOURCES/ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 15852ea6958c18e3830aa9244b36cd0decc93b95 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 7 Jan 2021 16:51:30 +0100 -Subject: [PATCH] Missing IPV6_AUTOCONF=no to render sysconfig dhcp6 stateful - on RHEL (#753) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 29: Missing IPV6_AUTOCONF=no to render sysconfig dhcp6 stateful on RHEL (#753) -RH-Commit: [1/1] 46943f83071d243bcc61f9d987b4fe7d9cf98596 (eterrell/cloud-init) -RH-Bugzilla: 1859695 - -IPV6_AUTOCONF needs to be set to 'no' on RHEL so NetworkManager can -properly acquire ipv6 address. - -rhbz: #1859695 - -Signed-off-by: Eduardo Otubo ---- - cloudinit/net/sysconfig.py | 1 + - tests/unittests/test_net.py | 1 + - 2 files changed, 2 insertions(+) - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 94801a93..1793977d 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -397,6 +397,7 @@ class Renderer(renderer.Renderer): - iface_cfg['BOOTPROTO'] = 'dhcp' - iface_cfg['DHCPV6C'] = True - iface_cfg['IPV6INIT'] = True -+ iface_cfg['IPV6_AUTOCONF'] = False - else: - iface_cfg['IPV6INIT'] = True - # Configure network settings using DHCPv6 -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index bcd261db..844d5ba8 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -1363,6 +1363,7 @@ NETWORK_CONFIGS = { - DEVICE=iface0 - DHCPV6C=yes - IPV6INIT=yes -+ IPV6_AUTOCONF=no - IPV6_FORCE_ACCEPT_RA=yes - DEVICE=iface0 - NM_CONTROLLED=no --- -2.18.4 - diff --git a/SOURCES/ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch b/SOURCES/ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch deleted file mode 100644 index a7f4117..0000000 --- a/SOURCES/ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch +++ /dev/null @@ -1,80 +0,0 @@ -From 4dde2a9bed58aba13c730bf4a7314b21038d7a31 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Mon, 25 Jan 2021 16:24:29 +0100 -Subject: [PATCH 2/2] Revert "ssh_util: handle non-default AuthorizedKeysFile - config (#586)" (#775) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 38: Revert "ssh_util: handle non-default AuthorizedKeysFile config (#586)" (#775) -RH-Commit: [1/1] aec2860c773ad1921f3949dc622543e81860c5bf (eterrell/cloud-init) -RH-Bugzilla: 1919972 - -commit cdc5b81f33aee0ed3ef1ae239e5cec1906d0178a -Author: Daniel Watkins -Date: Tue Jan 19 12:23:23 2021 -0500 - - Revert "ssh_util: handle non-default AuthorizedKeysFile config (#586)" (#775) - - This reverts commit b0e73814db4027dba0b7dc0282e295b7f653325c. - -Signed-off-by: Eduardo Otubo ---- - cloudinit/ssh_util.py | 6 +++--- - tests/unittests/test_sshutil.py | 6 +++--- - 2 files changed, 6 insertions(+), 6 deletions(-) - -diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py -index d5113996..c08042d6 100644 ---- a/cloudinit/ssh_util.py -+++ b/cloudinit/ssh_util.py -@@ -262,13 +262,13 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - - except (IOError, OSError): - # Give up and use a default key filename -- auth_key_fns.append(default_authorizedkeys_file) -+ auth_key_fns[0] = default_authorizedkeys_file - util.logexc(LOG, "Failed extracting 'AuthorizedKeysFile' in SSH " - "config from %r, using 'AuthorizedKeysFile' file " - "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) - -- # always store all the keys in the first file configured on sshd_config -- return (auth_key_fns[0], parse_authorized_keys(auth_key_fns)) -+ # always store all the keys in the user's private file -+ return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) - - - def setup_user_keys(keys, username, options=None): -diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py -index 88a111e3..fd1d1bac 100644 ---- a/tests/unittests/test_sshutil.py -+++ b/tests/unittests/test_sshutil.py -@@ -593,7 +593,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - fpw.pw_name, sshd_config) - content = ssh_util.update_authorized_keys(auth_key_entries, []) - -- self.assertEqual(authorized_keys, auth_key_fn) -+ self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) - self.assertTrue(VALID_CONTENT['rsa'] in content) - self.assertTrue(VALID_CONTENT['dsa'] in content) - -@@ -610,7 +610,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - sshd_config = self.tmp_path('sshd_config') - util.write_file( - sshd_config, -- "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys) -+ "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys) - ) - - (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys( -@@ -618,7 +618,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - ) - content = ssh_util.update_authorized_keys(auth_key_entries, []) - -- self.assertEqual(user_keys, auth_key_fn) -+ self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn) - self.assertTrue(VALID_CONTENT['rsa'] in content) - self.assertTrue(VALID_CONTENT['dsa'] in content) - --- -2.18.4 - 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 new file mode 100644 index 0000000..e46b52b --- /dev/null +++ b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch @@ -0,0 +1,1385 @@ +From 3b68aff3b7b1dc567ef6721a269c2d4e054b729f Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +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 +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 +RH-Acked-by: Eduardo Otubo + +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 +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/. 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 +--- + 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//.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-fix-a-typo-in-man-page-cloud-init.1-752.patch b/SOURCES/ci-fix-a-typo-in-man-page-cloud-init.1-752.patch deleted file mode 100644 index 0a08abf..0000000 --- a/SOURCES/ci-fix-a-typo-in-man-page-cloud-init.1-752.patch +++ /dev/null @@ -1,53 +0,0 @@ -From c90d5c11eb99ec25e0fd90585bad9283e60bda7e Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Tue, 26 Jan 2021 10:48:55 +0100 -Subject: [PATCH] fix a typo in man page cloud-init.1 (#752) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 39: fix a typo in man page cloud-init.1 (#752) -RH-Commit: [1/1] d2f7efbc63a7928ef175ac0714053dba20aab01a (eterrell/cloud-init) -RH-Bugzilla: 1913127 - -commit 48b2c5f16bd4ef754fef137ea19894908d4bf1db -Author: Amy Chen <66719270+xiachen-rh@users.noreply.github.com> -Date: Wed Jan 6 22:37:02 2021 +0800 - - fix a typo in man page cloud-init.1 (#752) - - 1. fix a typo in cloud-init.1 - 2. add xiachen-rh as contributor - -Conflict: We don't really use tools/.github-cla-signers, but had to fix -a tiny conflict of already included names on the file. - -Signed-off-by: Eduardo Otubo ---- - doc/man/cloud-init.1 | 2 +- - tools/.github-cla-signers | 1 + - 2 files changed, 2 insertions(+), 1 deletion(-) - -diff --git a/doc/man/cloud-init.1 b/doc/man/cloud-init.1 -index 9b52dc8d..3fde4148 100644 ---- a/doc/man/cloud-init.1 -+++ b/doc/man/cloud-init.1 -@@ -10,7 +10,7 @@ cloud-init \- Cloud instance initialization - Cloud-init provides a mechanism for cloud instance initialization. - This is done by identifying the cloud platform that is in use, reading - provided cloud metadata and optional vendor and user --data, and then intializing the instance as requested. -+data, and then initializing the instance as requested. - - Generally, this command is not normally meant to be run directly by - the user. However, some subcommands may useful for development or -diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers -index 802a35bd..e5d2b95c 100644 ---- a/tools/.github-cla-signers -+++ b/tools/.github-cla-signers -@@ -21,3 +21,4 @@ sshedi - TheRealFalcon - tomponline - tsanghan -+xiachen-rh --- -2.18.4 - diff --git a/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch b/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch deleted file mode 100644 index a2ef2dc..0000000 --- a/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch +++ /dev/null @@ -1,247 +0,0 @@ -From 51a90ecbdf1f3900183d8ec641eeb4571decf6dc Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Wed, 4 Nov 2020 12:37:54 +0100 -Subject: [PATCH] network: Fix type and respect name when rendering vlan in - sysconfig. (#541) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 19: network: Fix type and respect name when rendering vlan in sysconfig. (#541) -RH-Commit: [1/1] 75bea46017397082c5763125a5f35806c2f840e9 (eterrell/cloud-init) -RH-Bugzilla: 1881462 - -commit 8439b191ec2f336d544cab86dba2860f969cd5b8 -Author: Eduardo Otubo -Date: Tue Sep 15 18:00:00 2020 +0200 - - network: Fix type and respect name when rendering vlan in sysconfig. (#541) - - Prior to this change, vlans were rendered in sysconfig with - 'TYPE=Ethernet', and incorrectly rendered the PHYSDEV based on - the name of the vlan device rather than the 'link' provided - in the network config. - - The change here fixes: - * rendering of TYPE=Ethernet for a vlan - * adds a warning if the configured device name is not supported - per the RHEL 7 docs "11.5. Naming Scheme for VLAN Interfaces" - - LP: #1788915 - LP: #1826608 - RHBZ: #1861871 - -Signed-off-by: Eduardo Otubo ---- - cloudinit/net/sysconfig.py | 32 +++++++++- - tests/unittests/test_distros/test_netconfig.py | 81 ++++++++++++++++++++++++++ - tests/unittests/test_net.py | 4 -- - 3 files changed, 112 insertions(+), 5 deletions(-) - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index c078898..078636a 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -99,6 +99,10 @@ class ConfigMap(object): - def __len__(self): - return len(self._conf) - -+ def skip_key_value(self, key, val): -+ """Skip the pair key, value if it matches a certain rule.""" -+ return False -+ - def to_string(self): - buf = io.StringIO() - buf.write(_make_header()) -@@ -106,6 +110,8 @@ class ConfigMap(object): - buf.write("\n") - for key in sorted(self._conf.keys()): - value = self._conf[key] -+ if self.skip_key_value(key, value): -+ continue - if isinstance(value, bool): - value = self._bool_map[value] - if not isinstance(value, str): -@@ -214,6 +220,7 @@ class NetInterface(ConfigMap): - 'bond': 'Bond', - 'bridge': 'Bridge', - 'infiniband': 'InfiniBand', -+ 'vlan': 'Vlan', - } - - def __init__(self, iface_name, base_sysconf_dir, templates, -@@ -267,6 +274,11 @@ class NetInterface(ConfigMap): - c.routes = self.routes.copy() - return c - -+ def skip_key_value(self, key, val): -+ if key == 'TYPE' and val == 'Vlan': -+ return True -+ return False -+ - - class Renderer(renderer.Renderer): - """Renders network information in a /etc/sysconfig format.""" -@@ -701,7 +713,16 @@ class Renderer(renderer.Renderer): - iface_cfg['ETHERDEVICE'] = iface_name[:iface_name.rfind('.')] - else: - iface_cfg['VLAN'] = True -- iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')] -+ iface_cfg.kind = 'vlan' -+ -+ rdev = iface['vlan-raw-device'] -+ supported = _supported_vlan_names(rdev, iface['vlan_id']) -+ if iface_name not in supported: -+ LOG.info( -+ "Name '%s' for vlan '%s' is not officially supported" -+ "by RHEL. Supported: %s", -+ iface_name, rdev, ' '.join(supported)) -+ iface_cfg['PHYSDEV'] = rdev - - iface_subnets = iface.get("subnets", []) - route_cfg = iface_cfg.routes -@@ -909,6 +930,15 @@ class Renderer(renderer.Renderer): - "\n".join(netcfg) + "\n", file_mode) - - -+def _supported_vlan_names(rdev, vid): -+ """Return list of supported names for vlan devices per RHEL doc -+ 11.5. Naming Scheme for VLAN Interfaces.""" -+ return [ -+ v.format(rdev=rdev, vid=int(vid)) -+ for v in ("{rdev}{vid:04}", "{rdev}{vid}", -+ "{rdev}.{vid:04}", "{rdev}.{vid}")] -+ -+ - def available(target=None): - sysconfig = available_sysconfig(target=target) - nm = available_nm(target=target) -diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py -index f9fc3a1..a1df066 100644 ---- a/tests/unittests/test_distros/test_netconfig.py -+++ b/tests/unittests/test_distros/test_netconfig.py -@@ -541,6 +541,87 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - V1_NET_CFG_IPV6, - expected_cfgs=expected_cfgs.copy()) - -+ def test_vlan_render_unsupported(self): -+ """Render officially unsupported vlan names.""" -+ cfg = { -+ 'version': 2, -+ 'ethernets': { -+ 'eth0': {'addresses': ["192.10.1.2/24"], -+ 'match': {'macaddress': "00:16:3e:60:7c:df"}}}, -+ 'vlans': { -+ 'infra0': {'addresses': ["10.0.1.2/16"], -+ 'id': 1001, 'link': 'eth0'}}, -+ } -+ expected_cfgs = { -+ self.ifcfg_path('eth0'): dedent("""\ -+ BOOTPROTO=none -+ DEVICE=eth0 -+ HWADDR=00:16:3e:60:7c:df -+ IPADDR=192.10.1.2 -+ NETMASK=255.255.255.0 -+ NM_CONTROLLED=no -+ ONBOOT=yes -+ TYPE=Ethernet -+ USERCTL=no -+ """), -+ self.ifcfg_path('infra0'): dedent("""\ -+ BOOTPROTO=none -+ DEVICE=infra0 -+ IPADDR=10.0.1.2 -+ NETMASK=255.255.0.0 -+ NM_CONTROLLED=no -+ ONBOOT=yes -+ PHYSDEV=eth0 -+ USERCTL=no -+ VLAN=yes -+ """), -+ self.control_path(): dedent("""\ -+ NETWORKING=yes -+ """), -+ } -+ self._apply_and_verify( -+ self.distro.apply_network_config, cfg, -+ expected_cfgs=expected_cfgs) -+ -+ def test_vlan_render(self): -+ cfg = { -+ 'version': 2, -+ 'ethernets': { -+ 'eth0': {'addresses': ["192.10.1.2/24"]}}, -+ 'vlans': { -+ 'eth0.1001': {'addresses': ["10.0.1.2/16"], -+ 'id': 1001, 'link': 'eth0'}}, -+ } -+ expected_cfgs = { -+ self.ifcfg_path('eth0'): dedent("""\ -+ BOOTPROTO=none -+ DEVICE=eth0 -+ IPADDR=192.10.1.2 -+ NETMASK=255.255.255.0 -+ NM_CONTROLLED=no -+ ONBOOT=yes -+ TYPE=Ethernet -+ USERCTL=no -+ """), -+ self.ifcfg_path('eth0.1001'): dedent("""\ -+ BOOTPROTO=none -+ DEVICE=eth0.1001 -+ IPADDR=10.0.1.2 -+ NETMASK=255.255.0.0 -+ NM_CONTROLLED=no -+ ONBOOT=yes -+ PHYSDEV=eth0 -+ USERCTL=no -+ VLAN=yes -+ """), -+ self.control_path(): dedent("""\ -+ NETWORKING=yes -+ """), -+ } -+ self._apply_and_verify( -+ self.distro.apply_network_config, cfg, -+ expected_cfgs=expected_cfgs) -+ - - class TestNetCfgDistroOpensuse(TestNetCfgDistroBase): - -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index d7a7a65..c033745 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -1656,7 +1656,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - DHCLIENT_SET_DEFAULT_ROUTE=no - ONBOOT=yes - PHYSDEV=bond0 -- TYPE=Ethernet - USERCTL=no - VLAN=yes"""), - 'ifcfg-br0': textwrap.dedent("""\ -@@ -1699,7 +1698,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - NETMASK1=255.255.255.0 - ONBOOT=yes - PHYSDEV=eth0 -- TYPE=Ethernet - USERCTL=no - VLAN=yes"""), - 'ifcfg-eth1': textwrap.dedent("""\ -@@ -2302,7 +2300,6 @@ iface bond0 inet6 static - NETMASK1=255.255.255.0 - ONBOOT=yes - PHYSDEV=en0 -- TYPE=Ethernet - USERCTL=no - VLAN=yes"""), - }, -@@ -3409,7 +3406,6 @@ USERCTL=no - NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=eno1 -- TYPE=Ethernet - USERCTL=no - VLAN=yes - """) --- -1.8.3.1 - 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 new file mode 100644 index 0000000..be1e283 --- /dev/null +++ b/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch @@ -0,0 +1,65 @@ +From abf1adeae8211f5acd87dc63b03b2ed995047efd Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +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 +RH-MergeRequest: 10: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in cloud.cfg +RH-Commit: [1/1] 6da989423b9b6e017afbac2f1af3649b0487310f +RH-Bugzilla: 1957532 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Cathy Avery +RH-Acked-by: Vitaly Kuznetsov +RH-Acked-by: Mohamed Gamal Morsy + +Currently genkeytypes in cloud.cfg is set to None, so together with +ssh_deletekeys=1 cloudinit on first boot it will just delete the existing +keys and not generate new ones. + +Just removing that property in cloud.cfg is not enough, because +settings.py provides another empty default value that will be used +instead, resulting to no key generated even when the property is not defined. + +Removing genkeytypes also in settings.py will default to GENERATE_KEY_NAMES, +but since we want only 'rsa', 'ecdsa' and 'ed25519', add back genkeytypes in +cloud.cfg with the above defaults. + +Also remove ssh_deletekeys in settings.py as we always need +to 1 (and it also defaults to 1). + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/settings.py | 2 -- + rhel/cloud.cfg | 2 +- + 2 files changed, 1 insertion(+), 3 deletions(-) + +diff --git a/cloudinit/settings.py b/cloudinit/settings.py +index 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-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 new file mode 100644 index 0000000..bdec823 --- /dev/null +++ b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch @@ -0,0 +1,653 @@ +From aeab67600eb2d5e483812620b56ce5fb031a57d6 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +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 +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 +RH-Acked-by: Mohamed Gamal Morsy + +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 +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 + Co-authored-by: James Falcon + + LP: #1911680 + RHBZ:1862967 + +Signed-off-by: Emanuele Giuseppe Esposito +--- + 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_util-handle-non-default-AuthorizedKeysFile-confi.patch b/SOURCES/ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch deleted file mode 100644 index 5fbcb0c..0000000 --- a/SOURCES/ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch +++ /dev/null @@ -1,98 +0,0 @@ -From b84a1e6d246bbb758f0530038612bd18eff71767 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Tue, 8 Dec 2020 13:27:22 +0100 -Subject: [PATCH 4/4] ssh_util: handle non-default AuthorizedKeysFile config - (#586) - -RH-Author: Eduardo Terrell Ferrari Otubo (eterrell) -RH-MergeRequest: 28: ssh_util: handle non-default AuthorizedKeysFile config (#586) -RH-Commit: [1/1] f7ce396e3002c53a3504e653b58810efb956aa26 (eterrell/cloud-init) -RH-Bugzilla: 1862967 - -commit b0e73814db4027dba0b7dc0282e295b7f653325c -Author: Eduardo Otubo -Date: Tue Oct 20 18:04:59 2020 +0200 - - ssh_util: handle non-default AuthorizedKeysFile config (#586) - - The following commit merged all ssh keys into a default user file - `~/.ssh/authorized_keys` in sshd_config had multiple files configured for - AuthorizedKeysFile: - - commit f1094b1a539044c0193165a41501480de0f8df14 - Author: Eduardo Otubo - Date: Thu Dec 5 17:37:35 2019 +0100 - - Multiple file fix for AuthorizedKeysFile config (#60) - - This commit ignored the case when sshd_config would have a single file for - AuthorizedKeysFile, but a non default configuration, for example - `~/.ssh/authorized_keys_foobar`. In this case cloud-init would grab all keys - from this file and write a new one, the default `~/.ssh/authorized_keys` - causing the bug. - - rhbz: #1862967 - - Signed-off-by: Eduardo Otubo - -Signed-off-by: Eduardo Otubo ---- - cloudinit/ssh_util.py | 6 +++--- - tests/unittests/test_sshutil.py | 6 +++--- - 2 files changed, 6 insertions(+), 6 deletions(-) - -diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py -index c08042d6..d5113996 100644 ---- a/cloudinit/ssh_util.py -+++ b/cloudinit/ssh_util.py -@@ -262,13 +262,13 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG): - - except (IOError, OSError): - # Give up and use a default key filename -- auth_key_fns[0] = default_authorizedkeys_file -+ auth_key_fns.append(default_authorizedkeys_file) - util.logexc(LOG, "Failed extracting 'AuthorizedKeysFile' in SSH " - "config from %r, using 'AuthorizedKeysFile' file " - "%r instead", DEF_SSHD_CFG, auth_key_fns[0]) - -- # always store all the keys in the user's private file -- return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns)) -+ # always store all the keys in the first file configured on sshd_config -+ return (auth_key_fns[0], parse_authorized_keys(auth_key_fns)) - - - def setup_user_keys(keys, username, options=None): -diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py -index fd1d1bac..88a111e3 100644 ---- a/tests/unittests/test_sshutil.py -+++ b/tests/unittests/test_sshutil.py -@@ -593,7 +593,7 @@ 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(authorized_keys, auth_key_fn) - self.assertTrue(VALID_CONTENT['rsa'] in content) - self.assertTrue(VALID_CONTENT['dsa'] in content) - -@@ -610,7 +610,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - sshd_config = self.tmp_path('sshd_config') - 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( -@@ -618,7 +618,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase): - ) - 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) - --- -2.18.4 - 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 new file mode 100644 index 0000000..5cf4671 --- /dev/null +++ b/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch @@ -0,0 +1,369 @@ +From 769b9f8c9b1ecc294a197575108ae7cb54ad7f4b Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +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 +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 +RH-Acked-by: Mohamed Gamal Morsy + +commit b794d426b9ab43ea9d6371477466070d86e10668 +Author: Daniel Watkins +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 +--- + 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 cedad04..6671452 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -5,8 +5,8 @@ %global debug_package %{nil} Name: cloud-init -Version: 20.3 -Release: 10%{?dist} +Version: 21.1 +Release: 7%{?dist} Summary: Cloud instance init scripts Group: System Environment/Base @@ -22,24 +22,18 @@ 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 -Patch8: ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch -Patch9: ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch -# For bz#1881462 - [rhel8][cloud-init] ifup bond0.504 Error: Connection activation failed: No suitable device found for this connection -Patch10: ci-network-Fix-type-and-respect-name-when-rendering-vla.patch -# For bz#1859695 - [Cloud-init] DHCPv6 assigned address is not added to VM's interface -Patch11: ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch -# For bz#1898943 - [rhel-8]cloud-final.service fails if NetworkManager not installed. -Patch12: ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.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 -Patch13: ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch -# For bz#1859695 - [Cloud-init] DHCPv6 assigned address is not added to VM's interface -Patch14: ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch -# For bz#1900892 - [Azure] Update existing user password RHEL8x -Patch15: ci-DataSourceAzure-update-password-for-defuser-if-exist.patch -# For bz#1919972 - [RHEL-8.4] ssh keys can be shared across users giving potential root access -Patch16: ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch -# For bz#1913127 - A typo in cloud-init man page -Patch17: ci-fix-a-typo-in-man-page-cloud-init.1-752.patch +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 BuildArch: noarch @@ -87,6 +81,7 @@ Requires: python3-six Requires: shadow-utils Requires: util-linux Requires: xfsprogs +Requires: dhcp-client %{?systemd_requires} @@ -193,8 +188,7 @@ if [ $1 -eq 0 ] ; then fi %postun -%systemd_postun - +%systemd_postun cloud-config.service cloud-config.target cloud-final.service cloud-init.service cloud-init.target cloud-init-local.service %files %license LICENSE @@ -231,6 +225,44 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Fri Aug 27 2021 Miroslav Rezanina - 21.1-7 +- ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch [bz#1995840] +- Resolves: bz#1995840 + ([cloudinit] Fix home permissions modified by ssh module) + +* Wed Aug 11 2021 Miroslav Rezanina - 21.1-6 +- ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch [bz#1862967] +- Resolves: bz#1862967 + ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure) + +* Fri Aug 06 2021 Miroslav Rezanina - 21.1-5 +- ci-Add-dhcp-client-as-a-dependency.patch [bz#1977385] +- Resolves: bz#1977385 + ([Azure][RHEL-8] cloud-init must require dhcp-client on Azure) + +* Mon Jul 19 2021 Miroslav Rezanina - 21.1-4 +- ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch [bz#1862967] +- Resolves: bz#1862967 + ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure) + +* Mon Jul 12 2021 Miroslav Rezanina - 21.1-3 +- ci-write-passwords-only-to-serial-console-lock-down-clo.patch [bz#1945891] +- Resolves: bz#1945891 + (CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-8]) + +* Fri Jun 11 2021 Miroslav Rezanina - 21.1-2 +- ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch [bz#1957532] +- ci-cloud-init.spec.template-update-systemd_postun-param.patch [bz#1952089] +- Resolves: bz#1957532 + ([cloud-init] From RHEL 82+ cloud-init no longer displays sshd keys fingerprints from instance launched from a backup image) +- Resolves: bz#1952089 + (cloud-init brew build fails on Fedora 33) + +* Thu May 27 2021 Miroslav Rezanina - 21.1-1.el8 +- Rebaes to 21.1 [bz#1958174] +- Resolves: bz#1958174 + ([RHEL-8.5.0] Rebase cloud-init to 21.1) + * Tue Feb 02 2021 Miroslav Rezanina - 20.3-10.el8 - ci-fix-a-typo-in-man-page-cloud-init.1-752.patch [bz#1913127] - Resolves: bz#1913127