diff --git a/.cloud-init.metadata b/.cloud-init.metadata
index 6803c51..0356978 100644
--- a/.cloud-init.metadata
+++ b/.cloud-init.metadata
@@ -1 +1 @@
-2ae378aa2ae23b34b0ff123623ba5e2fbdc4928d SOURCES/cloud-init-21.1.tar.gz
+830185bb5ce87ad86e4d1c0c62329bb255ec1648 SOURCES/cloud-init-22.1.tar.gz
diff --git a/.gitignore b/.gitignore
index 103bcf7..bf19bdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-SOURCES/cloud-init-21.1.tar.gz
+SOURCES/cloud-init-22.1.tar.gz
diff --git a/SOURCES/0001-Add-initial-redhat-setup.patch b/SOURCES/0001-Add-initial-redhat-setup.patch
index b67fcae..d93c32c 100644
--- a/SOURCES/0001-Add-initial-redhat-setup.patch
+++ b/SOURCES/0001-Add-initial-redhat-setup.patch
@@ -1,8 +1,34 @@
-From 074cb9b011623849cfa95c1d7cc813bb28f03ff0 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Fri, 7 May 2021 13:36:03 +0200
+From 5e1e568d7085fd4443b4e3ccc492f5e31747e270 Mon Sep 17 00:00:00 2001
+From: Amy Chen <xiachen@redhat.com>
+Date: Wed, 20 Apr 2022 10:59:48 +0800
 Subject: Add initial redhat setup
 
+Merged patches (22.1)
+- d18029bf Add netifaces package as a Requires in cloud-init.spec.template
+- 31adf961 Add gdisk and openssl as deps to fix UEFI / Azure initialization
+- f4a2905d Add dhcp-client as a dependency
+- 290e14cc cloud-init.spec.template: update %systemd_postun parameter
+- 9be4ae9b (tag: cloud-init-21.1-1.el8) Update to cloud-init-21.1-1.el8
+
+Conflicts:
+cloudinit/config/cc_chef.py  Using double quotes instead of single quotes
+
+cloudinit/settings.py
+- Using rhel settings
+- Using double quotes instead of single quotes
+
+setup.py
+- Following the changes of 21.1 rebase
+- Using double quotes instead of single quotes
+
+redhat/cloud-init.spec.template
+- Add the drop-in to the right cloud-init.spec used by our package builder, which is downstream-only part of the bz 2002492 fix.
+
+redhat/Makefile.common
+- Backport the build handling fixes from patch "Update to cloud-init-21.1-1.el8"
+
+Signed-off-by: Amy Chen <xiachen@redhat.com>
+
 Merged patches (21.1):
 - 915d30ad Change gating file to correct rhel version
 - 311f318d Removing net-tools dependency
@@ -43,36 +69,36 @@ setup.py:
 Signed-off-by: Eduardo Otubo <otubo@redhat.com>
 ---
  .gitignore                            |   1 +
- cloudinit/config/cc_chef.py           |  67 +++-
+ cloudinit/config/cc_chef.py           |  65 ++-
  cloudinit/settings.py                 |   7 +-
  redhat/.gitignore                     |   1 +
- redhat/Makefile                       |  71 ++++
+ redhat/Makefile                       |  71 +++
  redhat/Makefile.common                |  37 ++
  redhat/cloud-init-tmpfiles.conf       |   1 +
- redhat/cloud-init.spec.template       | 530 ++++++++++++++++++++++++++
+ redhat/cloud-init.spec.template       | 696 ++++++++++++++++++++++++++
  redhat/gating.yaml                    |   8 +
  redhat/rpmbuild/BUILD/.gitignore      |   3 +
  redhat/rpmbuild/RPMS/.gitignore       |   3 +
  redhat/rpmbuild/SOURCES/.gitignore    |   3 +
  redhat/rpmbuild/SPECS/.gitignore      |   3 +
  redhat/rpmbuild/SRPMS/.gitignore      |   3 +
- redhat/scripts/frh.py                 |  27 ++
- redhat/scripts/git-backport-diff      | 327 ++++++++++++++++
- redhat/scripts/git-compile-check      | 215 +++++++++++
- redhat/scripts/process-patches.sh     |  77 ++++
+ redhat/scripts/frh.py                 |  25 +
+ redhat/scripts/git-backport-diff      | 327 ++++++++++++
+ redhat/scripts/git-compile-check      | 215 ++++++++
+ redhat/scripts/process-patches.sh     |  92 ++++
  redhat/scripts/tarball_checksum.sh    |   3 +
  rhel/README.rhel                      |   5 +
  rhel/cloud-init-tmpfiles.conf         |   1 +
- rhel/cloud.cfg                        |  69 ++++
+ rhel/cloud.cfg                        |  69 +++
  rhel/systemd/cloud-config.service     |  18 +
  rhel/systemd/cloud-config.target      |  11 +
- rhel/systemd/cloud-final.service      |  24 ++
+ rhel/systemd/cloud-final.service      |  24 +
  rhel/systemd/cloud-init-local.service |  31 ++
- rhel/systemd/cloud-init.service       |  25 ++
+ rhel/systemd/cloud-init.service       |  25 +
  rhel/systemd/cloud-init.target        |   7 +
- setup.py                              |  23 +-
+ setup.py                              |  28 +-
  tools/read-version                    |  28 +-
- 30 files changed, 1579 insertions(+), 50 deletions(-)
+ 30 files changed, 1756 insertions(+), 55 deletions(-)
  create mode 100644 redhat/.gitignore
  create mode 100644 redhat/Makefile
  create mode 100644 redhat/Makefile.common
@@ -100,7 +126,7 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com>
  create mode 100644 rhel/systemd/cloud-init.target
 
 diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
-index aaf71366..97ef649a 100644
+index fdb3a6e3..d028c548 100644
 --- a/cloudinit/config/cc_chef.py
 +++ b/cloudinit/config/cc_chef.py
 @@ -6,7 +6,70 @@
@@ -175,38 +201,29 @@ index aaf71366..97ef649a 100644
  
  import itertools
  import json
-@@ -31,7 +94,7 @@ CHEF_DIRS = tuple([
-     '/var/lib/chef',
-     '/var/cache/chef',
-     '/var/backups/chef',
--    '/var/run/chef',
-+    '/run/chef',
- ])
- REQUIRED_CHEF_DIRS = tuple([
-     '/etc/chef',
 diff --git a/cloudinit/settings.py b/cloudinit/settings.py
-index 91e1bfe7..e690c0fd 100644
+index ecc1403b..39650a5b 100644
 --- a/cloudinit/settings.py
 +++ b/cloudinit/settings.py
-@@ -47,13 +47,16 @@ CFG_BUILTIN = {
+@@ -50,13 +50,16 @@ CFG_BUILTIN = {
      ],
-     'def_log_file': '/var/log/cloud-init.log',
-     'log_cfgs': [],
--    'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel', 'root:root'],
-+    'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'],
-+    'ssh_deletekeys': False,
-+    'ssh_genkeytypes': [],
-+    'syslog_fix_perms': [],
-     'system_info': {
-         'paths': {
-             'cloud_dir': '/var/lib/cloud',
-             'templates_dir': '/etc/cloud/templates/',
+     "def_log_file": "/var/log/cloud-init.log",
+     "log_cfgs": [],
+-    "syslog_fix_perms": ["syslog:adm", "root:adm", "root:wheel", "root:root"],
++    "mount_default_fields": [None, None, "auto", "defaults,nofail", "0", "2"],
++    "ssh_deletekeys": False,
++    "ssh_genkeytypes": [],
++    "syslog_fix_perms": [],
+     "system_info": {
+         "paths": {
+             "cloud_dir": "/var/lib/cloud",
+             "templates_dir": "/etc/cloud/templates/",
          },
--        'distro': 'ubuntu',
-+        'distro': 'rhel',
-         'network': {'renderers': None},
+-        "distro": "ubuntu",
++        "distro": "rhel",
+         "network": {"renderers": None},
      },
-     'vendor_data': {'enabled': True, 'prefix': []},
+     "vendor_data": {"enabled": True, "prefix": []},
 diff --git a/rhel/README.rhel b/rhel/README.rhel
 new file mode 100644
 index 00000000..aa29630d
@@ -453,70 +470,78 @@ index 00000000..083c3b6f
 +Description=Cloud-init target
 +After=multi-user.target
 diff --git a/setup.py b/setup.py
-index cbacf48e..d5cd01a4 100755
+index a9132d2c..3c377eaa 100755
 --- a/setup.py
 +++ b/setup.py
-@@ -125,14 +125,6 @@ INITSYS_FILES = {
-     'sysvinit_deb': [f for f in glob('sysvinit/debian/*') if is_f(f)],
-     'sysvinit_openrc': [f for f in glob('sysvinit/gentoo/*') if is_f(f)],
-     'sysvinit_suse': [f for f in glob('sysvinit/suse/*') if is_f(f)],
--    'systemd': [render_tmpl(f)
--                for f in (glob('systemd/*.tmpl') +
--                          glob('systemd/*.service') +
--                          glob('systemd/*.target'))
--                if (is_f(f) and not is_generator(f))],
--    'systemd.generators': [
+@@ -139,21 +139,6 @@ INITSYS_FILES = {
+     "sysvinit_deb": [f for f in glob("sysvinit/debian/*") if is_f(f)],
+     "sysvinit_openrc": [f for f in glob("sysvinit/gentoo/*") if is_f(f)],
+     "sysvinit_suse": [f for f in glob("sysvinit/suse/*") if is_f(f)],
+-    "systemd": [
+-        render_tmpl(f)
+-        for f in (
+-            glob("systemd/*.tmpl")
+-            + glob("systemd/*.service")
+-            + glob("systemd/*.socket")
+-            + glob("systemd/*.target")
+-        )
+-        if (is_f(f) and not is_generator(f))
+-    ],
+-    "systemd.generators": [
 -        render_tmpl(f, mode=0o755)
--        for f in glob('systemd/*') if is_f(f) and is_generator(f)],
-     'upstart': [f for f in glob('upstart/*') if is_f(f)],
+-        for f in glob("systemd/*")
+-        if is_f(f) and is_generator(f)
+-    ],
+     "upstart": [f for f in glob("upstart/*") if is_f(f)],
  }
  INITSYS_ROOTS = {
-@@ -142,9 +134,6 @@ INITSYS_ROOTS = {
-     'sysvinit_deb': 'etc/init.d',
-     'sysvinit_openrc': 'etc/init.d',
-     'sysvinit_suse': 'etc/init.d',
--    'systemd': pkg_config_read('systemd', 'systemdsystemunitdir'),
--    'systemd.generators': pkg_config_read('systemd',
--                                          'systemdsystemgeneratordir'),
-     'upstart': 'etc/init/',
+@@ -163,10 +148,6 @@ INITSYS_ROOTS = {
+     "sysvinit_deb": "etc/init.d",
+     "sysvinit_openrc": "etc/init.d",
+     "sysvinit_suse": "etc/init.d",
+-    "systemd": pkg_config_read("systemd", "systemdsystemunitdir"),
+-    "systemd.generators": pkg_config_read(
+-        "systemd", "systemdsystemgeneratordir"
+-    ),
+     "upstart": "etc/init/",
  }
  INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()])
-@@ -245,14 +234,11 @@ if not in_virtualenv():
-         INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k]
- 
- data_files = [
--    (ETC + '/cloud', [render_tmpl("config/cloud.cfg.tmpl")]),
-+    (ETC + '/bash_completion.d', ['bash_completion/cloud-init']),
-     (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
-     (ETC + '/cloud/templates', glob('templates/*')),
--    (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify',
--                                    'tools/uncloud-init',
-+    (USR_LIB_EXEC + '/cloud-init', ['tools/uncloud-init',
-                                     'tools/write-ssh-key-fingerprints']),
--    (USR + '/share/bash-completion/completions',
--     ['bash_completion/cloud-init']),
-     (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]),
-     (USR + '/share/doc/cloud-init/examples',
-         [f for f in glob('doc/examples/*') if is_f(f)]),
-@@ -263,8 +249,7 @@ if not platform.system().endswith('BSD'):
-     data_files.extend([
-         (ETC + '/NetworkManager/dispatcher.d/',
-          ['tools/hook-network-manager']),
--        (ETC + '/dhcp/dhclient-exit-hooks.d/', ['tools/hook-dhclient']),
--        (LIB + '/udev/rules.d', [f for f in glob('udev/*.rules')])
-+        ('/usr/lib/udev/rules.d', [f for f in glob('udev/*.rules')])
-     ])
- # Use a subclass for install that handles
- # adding on the right init system configuration files
-@@ -286,8 +271,6 @@ setuptools.setup(
-     scripts=['tools/cloud-init-per'],
-     license='Dual-licensed under GPLv3 or Apache 2.0',
+@@ -281,15 +262,13 @@ data_files = [
+     (
+         USR_LIB_EXEC + "/cloud-init",
+         [
+-            "tools/ds-identify",
+             "tools/hook-hotplug",
+             "tools/uncloud-init",
+             "tools/write-ssh-key-fingerprints",
+         ],
+     ),
+     (
+-        USR + "/share/bash-completion/completions",
+-        ["bash_completion/cloud-init"],
++        ETC + "/bash_completion.d", ["bash_completion/cloud-init"],
+     ),
+     (USR + "/share/doc/cloud-init", [f for f in glob("doc/*") if is_f(f)]),
+     (
+@@ -308,8 +287,7 @@ if not platform.system().endswith("BSD"):
+                 ETC + "/NetworkManager/dispatcher.d/",
+                 ["tools/hook-network-manager"],
+             ),
+-            (ETC + "/dhcp/dhclient-exit-hooks.d/", ["tools/hook-dhclient"]),
+-            (LIB + "/udev/rules.d", [f for f in glob("udev/*.rules")]),
++            ("/usr/lib/udev/rules.d", [f for f in glob("udev/*.rules")]),
+             (
+                 ETC + "/systemd/system/sshd-keygen@.service.d/",
+                 ["systemd/disable-sshd-keygen-if-cloud-init-active.conf"],
+@@ -339,8 +317,6 @@ setuptools.setup(
+     scripts=["tools/cloud-init-per"],
+     license="Dual-licensed under GPLv3 or Apache 2.0",
      data_files=data_files,
 -    install_requires=requirements,
 -    cmdclass=cmdclass,
      entry_points={
-         'console_scripts': [
-             'cloud-init = cloudinit.cmd.main:main',
+         "console_scripts": [
+             "cloud-init = cloudinit.cmd.main:main",
 diff --git a/tools/read-version b/tools/read-version
 index 02c90643..79755f78 100755
 --- a/tools/read-version
@@ -557,5 +582,5 @@ index 02c90643..79755f78 100755
  # version is X.Y.Z[+xxx.gHASH]
  # version_long is None or X.Y.Z-xxx-gHASH
 -- 
-2.27.0
+2.31.1
 
diff --git a/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch b/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
index 3dc704f..60c0a2a 100644
--- a/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
+++ b/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
@@ -1,4 +1,4 @@
-From 472c2b5d4342b6ab6ce1584dc39bed0e6c1ca2e7 Mon Sep 17 00:00:00 2001
+From e0dc628ac553072891fa6607dc91b652efd99be2 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
 Date: Fri, 7 May 2021 13:36:06 +0200
 Subject: Do not write NM_CONTROLLED=no in generated interface config  files
@@ -12,28 +12,27 @@ X-downstream-only: true
 Signed-off-by: Eduardo Otubo <otubo@redhat.com>
 Signed-off-by: Ryan McCabe <rmccabe@redhat.com>
 ---
- cloudinit/net/sysconfig.py  |  2 +-
+ cloudinit/net/sysconfig.py  |  1 -
  tests/unittests/test_net.py | 28 ----------------------------
- 2 files changed, 1 insertion(+), 29 deletions(-)
+ 2 files changed, 29 deletions(-)
 
 diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index 99a4bae4..3d276666 100644
+index ba85c4f6..e06ddee7 100644
 --- a/cloudinit/net/sysconfig.py
 +++ b/cloudinit/net/sysconfig.py
-@@ -289,7 +289,7 @@ class Renderer(renderer.Renderer):
-     #                                         details about this)
- 
-     iface_defaults = {
--        'rhel': {'ONBOOT': True, 'USERCTL': False, 'NM_CONTROLLED': False,
-+        'rhel': {'ONBOOT': True, 'USERCTL': False,
-                  'BOOTPROTO': 'none'},
-         'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'},
-     }
+@@ -336,7 +336,6 @@ class Renderer(renderer.Renderer):
+         "rhel": {
+             "ONBOOT": True,
+             "USERCTL": False,
+-            "NM_CONTROLLED": False,
+             "BOOTPROTO": "none",
+         },
+         "suse": {"BOOTPROTO": "static", "STARTMODE": "auto"},
 diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 38d934d4..c67b5fcc 100644
+index 47e4ba00..591241b3 100644
 --- a/tests/unittests/test_net.py
 +++ b/tests/unittests/test_net.py
-@@ -535,7 +535,6 @@ GATEWAY=172.19.3.254
+@@ -579,7 +579,6 @@ GATEWAY=172.19.3.254
  HWADDR=fa:16:3e:ed:9a:59
  IPADDR=172.19.1.34
  NETMASK=255.255.252.0
@@ -41,7 +40,7 @@ index 38d934d4..c67b5fcc 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -633,7 +632,6 @@ IPADDR=172.19.1.34
+@@ -712,7 +711,6 @@ IPADDR=172.19.1.34
  IPADDR1=10.0.0.10
  NETMASK=255.255.252.0
  NETMASK1=255.255.255.0
@@ -49,7 +48,7 @@ index 38d934d4..c67b5fcc 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -756,7 +754,6 @@ IPV6_AUTOCONF=no
+@@ -874,7 +872,6 @@ IPV6_AUTOCONF=no
  IPV6_DEFAULTGW=2001:DB8::1
  IPV6_FORCE_ACCEPT_RA=no
  NETMASK=255.255.252.0
@@ -57,23 +56,23 @@ index 38d934d4..c67b5fcc 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -884,7 +881,6 @@ NETWORK_CONFIGS = {
+@@ -1053,7 +1050,6 @@ NETWORK_CONFIGS = {
                  BOOTPROTO=none
                  DEVICE=eth1
                  HWADDR=cf:d6:af:48:e8:80
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -901,7 +897,6 @@ NETWORK_CONFIGS = {
+                 USERCTL=no"""
+@@ -1072,7 +1068,6 @@ NETWORK_CONFIGS = {
                  IPADDR=192.168.21.3
                  NETMASK=255.255.255.0
                  METRIC=10000
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -1032,7 +1027,6 @@ NETWORK_CONFIGS = {
+                 USERCTL=no"""
+@@ -1244,7 +1239,6 @@ NETWORK_CONFIGS = {
                  IPV6_AUTOCONF=no
                  IPV6_FORCE_ACCEPT_RA=no
                  NETMASK=255.255.255.0
@@ -81,15 +80,15 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no
-@@ -1737,7 +1731,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -2093,7 +2087,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DHCPV6C=yes
                  IPV6INIT=yes
                  MACADDR=aa:bb:cc:dd:ee:ff
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Bond
-                 USERCTL=no"""),
-@@ -1745,7 +1738,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+                 USERCTL=no"""
+@@ -2103,7 +2096,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BOOTPROTO=dhcp
                  DEVICE=bond0.200
                  DHCLIENT_SET_DEFAULT_ROUTE=no
@@ -97,7 +96,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  PHYSDEV=bond0
                  USERCTL=no
-@@ -1763,7 +1755,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -2123,7 +2115,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  IPV6_DEFAULTGW=2001:4800:78ff:1b::1
                  MACADDR=bb:bb:bb:bb:bb:aa
                  NETMASK=255.255.255.0
@@ -105,15 +104,15 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  PRIO=22
                  STP=no
-@@ -1773,7 +1764,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -2135,7 +2126,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BOOTPROTO=none
                  DEVICE=eth0
                  HWADDR=c0:d6:9f:2c:e8:80
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -1790,7 +1780,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+                 USERCTL=no"""
+@@ -2154,7 +2144,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  MTU=1500
                  NETMASK=255.255.255.0
                  NETMASK1=255.255.255.0
@@ -121,7 +120,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  PHYSDEV=eth0
                  USERCTL=no
-@@ -1800,7 +1789,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -2166,7 +2155,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DEVICE=eth1
                  HWADDR=aa:d6:9f:2c:e8:80
                  MASTER=bond0
@@ -129,7 +128,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  SLAVE=yes
                  TYPE=Ethernet
-@@ -1810,7 +1798,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -2178,7 +2166,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DEVICE=eth2
                  HWADDR=c0:bb:9f:2c:e8:80
                  MASTER=bond0
@@ -137,31 +136,31 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  SLAVE=yes
                  TYPE=Ethernet
-@@ -1820,7 +1807,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -2190,7 +2177,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BRIDGE=br0
                  DEVICE=eth3
                  HWADDR=66:bb:9f:2c:e8:80
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -1829,7 +1815,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+                 USERCTL=no"""
+@@ -2201,7 +2187,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BRIDGE=br0
                  DEVICE=eth4
                  HWADDR=98:bb:9f:2c:e8:80
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -1838,7 +1823,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+                 USERCTL=no"""
+@@ -2212,7 +2197,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DEVICE=eth5
                  DHCLIENT_SET_DEFAULT_ROUTE=no
                  HWADDR=98:bb:9f:2c:e8:8a
 -                NM_CONTROLLED=no
                  ONBOOT=no
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -2294,7 +2278,6 @@ iface bond0 inet6 static
+                 USERCTL=no"""
+@@ -2689,7 +2673,6 @@ iface bond0 inet6 static
          MTU=9000
          NETMASK=255.255.255.0
          NETMASK1=255.255.255.0
@@ -169,7 +168,7 @@ index 38d934d4..c67b5fcc 100644
          ONBOOT=yes
          TYPE=Bond
          USERCTL=no
-@@ -2304,7 +2287,6 @@ iface bond0 inet6 static
+@@ -2701,7 +2684,6 @@ iface bond0 inet6 static
          DEVICE=bond0s0
          HWADDR=aa:bb:cc:dd:e8:00
          MASTER=bond0
@@ -177,7 +176,7 @@ index 38d934d4..c67b5fcc 100644
          ONBOOT=yes
          SLAVE=yes
          TYPE=Ethernet
-@@ -2326,7 +2308,6 @@ iface bond0 inet6 static
+@@ -2729,7 +2711,6 @@ iface bond0 inet6 static
          DEVICE=bond0s1
          HWADDR=aa:bb:cc:dd:e8:01
          MASTER=bond0
@@ -185,15 +184,15 @@ index 38d934d4..c67b5fcc 100644
          ONBOOT=yes
          SLAVE=yes
          TYPE=Ethernet
-@@ -2383,7 +2364,6 @@ iface bond0 inet6 static
+@@ -2794,7 +2775,6 @@ iface bond0 inet6 static
                  BOOTPROTO=none
                  DEVICE=en0
                  HWADDR=aa:bb:cc:dd:e8:00
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
-                 USERCTL=no"""),
-@@ -2402,7 +2382,6 @@ iface bond0 inet6 static
+                 USERCTL=no"""
+@@ -2815,7 +2795,6 @@ iface bond0 inet6 static
                  MTU=2222
                  NETMASK=255.255.255.0
                  NETMASK1=255.255.255.0
@@ -201,7 +200,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  PHYSDEV=en0
                  USERCTL=no
-@@ -2467,7 +2446,6 @@ iface bond0 inet6 static
+@@ -2890,7 +2869,6 @@ iface bond0 inet6 static
                  DEVICE=br0
                  IPADDR=192.168.2.2
                  NETMASK=255.255.255.0
@@ -209,7 +208,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  PRIO=22
                  STP=no
-@@ -2591,7 +2569,6 @@ iface bond0 inet6 static
+@@ -3032,7 +3010,6 @@ iface bond0 inet6 static
                  HWADDR=52:54:00:12:34:00
                  IPADDR=192.168.1.2
                  NETMASK=255.255.255.0
@@ -217,7 +216,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=no
                  TYPE=Ethernet
                  USERCTL=no
-@@ -2601,7 +2578,6 @@ iface bond0 inet6 static
+@@ -3044,7 +3021,6 @@ iface bond0 inet6 static
                  DEVICE=eth1
                  HWADDR=52:54:00:12:34:aa
                  MTU=1480
@@ -225,7 +224,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no
-@@ -2610,7 +2586,6 @@ iface bond0 inet6 static
+@@ -3055,7 +3031,6 @@ iface bond0 inet6 static
                  BOOTPROTO=none
                  DEVICE=eth2
                  HWADDR=52:54:00:12:34:ff
@@ -233,7 +232,7 @@ index 38d934d4..c67b5fcc 100644
                  ONBOOT=no
                  TYPE=Ethernet
                  USERCTL=no
-@@ -3027,7 +3002,6 @@ class TestRhelSysConfigRendering(CiTestCase):
+@@ -3628,7 +3603,6 @@ class TestRhelSysConfigRendering(CiTestCase):
  BOOTPROTO=dhcp
  DEVICE=eth1000
  HWADDR=07-1c-c6-75-a4-be
@@ -241,7 +240,7 @@ index 38d934d4..c67b5fcc 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -3148,7 +3122,6 @@ GATEWAY=10.0.2.2
+@@ -3840,7 +3814,6 @@ GATEWAY=10.0.2.2
  HWADDR=52:54:00:12:34:00
  IPADDR=10.0.2.15
  NETMASK=255.255.255.0
@@ -249,7 +248,7 @@ index 38d934d4..c67b5fcc 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -3218,7 +3191,6 @@ USERCTL=no
+@@ -3910,7 +3883,6 @@ USERCTL=no
  #
  BOOTPROTO=dhcp
  DEVICE=eth0
@@ -258,5 +257,5 @@ index 38d934d4..c67b5fcc 100644
  TYPE=Ethernet
  USERCTL=no
 -- 
-2.27.0
+2.31.1
 
diff --git a/SOURCES/0003-limit-permissions-on-def_log_file.patch b/SOURCES/0003-limit-permissions-on-def_log_file.patch
index 941adaf..6f58247 100644
--- a/SOURCES/0003-limit-permissions-on-def_log_file.patch
+++ b/SOURCES/0003-limit-permissions-on-def_log_file.patch
@@ -1,4 +1,4 @@
-From 6134624f10ef56534e37624adc12f11b09910591 Mon Sep 17 00:00:00 2001
+From cb7b35ca10c82c9725c3527e3ec5fb8cb7c61bc0 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
 Date: Fri, 7 May 2021 13:36:08 +0200
 Subject: limit permissions on def_log_file
@@ -22,31 +22,31 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com>
  3 files changed, 6 insertions(+)
 
 diff --git a/cloudinit/settings.py b/cloudinit/settings.py
-index e690c0fd..43a1490c 100644
+index 39650a5b..3c2145e9 100644
 --- a/cloudinit/settings.py
 +++ b/cloudinit/settings.py
-@@ -46,6 +46,7 @@ CFG_BUILTIN = {
-         'None',
+@@ -49,6 +49,7 @@ CFG_BUILTIN = {
+         "None",
      ],
-     'def_log_file': '/var/log/cloud-init.log',
-+    'def_log_file_mode': 0o600,
-     'log_cfgs': [],
-     'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'],
-     'ssh_deletekeys': False,
+     "def_log_file": "/var/log/cloud-init.log",
++    "def_log_file_mode": 0o600,
+     "log_cfgs": [],
+     "mount_default_fields": [None, None, "auto", "defaults,nofail", "0", "2"],
+     "ssh_deletekeys": False,
 diff --git a/cloudinit/stages.py b/cloudinit/stages.py
-index 3ef4491c..83e25dd1 100644
+index 3f17294b..61db1dbd 100644
 --- a/cloudinit/stages.py
 +++ b/cloudinit/stages.py
-@@ -147,6 +147,7 @@ class Init(object):
+@@ -205,6 +205,7 @@ class Init(object):
      def _initialize_filesystem(self):
          util.ensure_dirs(self._initial_subdirs())
-         log_file = util.get_cfg_option_str(self.cfg, 'def_log_file')
-+        log_file_mode = util.get_cfg_option_int(self.cfg, 'def_log_file_mode')
+         log_file = util.get_cfg_option_str(self.cfg, "def_log_file")
++        log_file_mode = util.get_cfg_option_int(self.cfg, "def_log_file_mode")
          if log_file:
-             util.ensure_file(log_file, preserve_mode=True)
-             perms = self.cfg.get('syslog_fix_perms')
+             util.ensure_file(log_file, mode=0o640, preserve_mode=True)
+             perms = self.cfg.get("syslog_fix_perms")
 diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt
-index de9a0f87..bb33ad45 100644
+index a2b4a3fa..0ccf3147 100644
 --- a/doc/examples/cloud-config.txt
 +++ b/doc/examples/cloud-config.txt
 @@ -414,10 +414,14 @@ timezone: US/Eastern
@@ -65,5 +65,5 @@ index de9a0f87..bb33ad45 100644
  
  # you can set passwords for a user or multiple users
 -- 
-2.27.0
+2.31.1
 
diff --git a/SOURCES/0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch b/SOURCES/0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
new file mode 100644
index 0000000..5c5a144
--- /dev/null
+++ b/SOURCES/0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
@@ -0,0 +1,52 @@
+From ffa647e83efd4293bd027e9e390274aad8a12d94 Mon Sep 17 00:00:00 2001
+From: Eduardo Otubo <otubo@redhat.com>
+Date: Fri, 7 May 2021 13:36:13 +0200
+Subject: include 'NOZEROCONF=yes' in /etc/sysconfig/network
+
+RH-Author: Eduardo Otubo <otubo@redhat.com>
+Message-id: <20190320114559.23708-1-otubo@redhat.com>
+Patchwork-id: 84937
+O-Subject: [RHEL-7.7 cloud-init PATCH] include 'NOZEROCONF=yes' in /etc/sysconfig/network
+Bugzilla: 1653131
+RH-Acked-by: Cathy Avery <cavery@redhat.com>
+RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
+RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+
+The option NOZEROCONF=yes is not included by default in
+/etc/sysconfig/network, which is required by Overcloud instances. The
+patch also includes tests for the modifications.
+
+X-downstream-only: yes
+Resolves: rhbz#1653131
+
+Signed-off-by: Eduardo Otubo <otubo@redhat.com>
+Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
+---
+ cloudinit/net/sysconfig.py | 11 ++++++++++-
+ 1 file changed, 10 insertions(+), 1 deletion(-)
+
+diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
+index e06ddee7..362e8d19 100644
+--- a/cloudinit/net/sysconfig.py
++++ b/cloudinit/net/sysconfig.py
+@@ -1038,7 +1038,16 @@ class Renderer(renderer.Renderer):
+         # Distros configuring /etc/sysconfig/network as a file e.g. Centos
+         if sysconfig_path.endswith("network"):
+             util.ensure_dir(os.path.dirname(sysconfig_path))
+-            netcfg = [_make_header(), "NETWORKING=yes"]
++            netcfg = []
++            for line in util.load_file(sysconfig_path, quiet=True).split("\n"):
++                if "cloud-init" in line:
++                    break
++                if not line.startswith(("NETWORKING=",
++                                        "IPV6_AUTOCONF=",
++                                        "NETWORKING_IPV6=")):
++                    netcfg.append(line)
++            # Now generate the cloud-init portion of sysconfig/network
++            netcfg.extend([_make_header(), "NETWORKING=yes"])
+             if network_state.use_ipv6:
+                 netcfg.append("NETWORKING_IPV6=yes")
+                 netcfg.append("IPV6_AUTOCONF=no")
+-- 
+2.31.1
+
diff --git a/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch b/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
deleted file mode 100644
index 4d5a0d2..0000000
--- a/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
+++ /dev/null
@@ -1,36 +0,0 @@
-From 699d37a6ff3e343e214943794aac09e4156c2b2b Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Fri, 7 May 2021 13:36:10 +0200
-Subject: sysconfig: Don't write BOOTPROTO=dhcp for ipv6 dhcp
-
-Don't write BOOTPROTO=dhcp for ipv6 dhcp, as BOOTPROTO applies
-only to ipv4. Explicitly write IPV6_AUTOCONF=no for dhcp on ipv6.
-
-X-downstream-only: yes
-
-Resolves: rhbz#1519271
-Signed-off-by: Ryan McCabe <rmccabe@redhat.com>
-
-Merged patches (19.4):
-- 6444df4 sysconfig: Don't disable IPV6_AUTOCONF
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- tests/unittests/test_net.py | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index c67b5fcc..4ea0e597 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -1729,6 +1729,7 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
-                 BOOTPROTO=none
-                 DEVICE=bond0
-                 DHCPV6C=yes
-+                IPV6_AUTOCONF=no
-                 IPV6INIT=yes
-                 MACADDR=aa:bb:cc:dd:ee:ff
-                 ONBOOT=yes
--- 
-2.27.0
-
diff --git a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch b/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
deleted file mode 100644
index 100d3a2..0000000
--- a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
+++ /dev/null
@@ -1,57 +0,0 @@
-From ccc75c1be3ae08d813193071c798fc905b5c03e5 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Fri, 7 May 2021 13:36:12 +0200
-Subject: DataSourceAzure.py: use hostnamectl to set hostname
-
-RH-Author: Vitaly Kuznetsov <vkuznets@redhat.com>
-Message-id: <20180417130754.12918-3-vkuznets@redhat.com>
-Patchwork-id: 79659
-O-Subject: [RHEL7.6/7.5.z cloud-init PATCH 2/2] DataSourceAzure.py: use hostnamectl to set hostname
-Bugzilla: 1568717
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
-RH-Acked-by: Cathy Avery <cavery@redhat.com>
-
-The right way to set hostname in RHEL7 is:
-
- $ hostnamectl set-hostname HOSTNAME
-
-DataSourceAzure, however, uses:
- $ hostname HOSTSNAME
-
-instead and this causes problems. We can't simply change
-'BUILTIN_DS_CONFIG' in DataSourceAzure.py as 'hostname' is being used
-for both getting and setting the hostname.
-
-Long term, this should be fixed in a different way. Cloud-init
-has distro-specific hostname setting/getting (see
-cloudinit/distros/rhel.py) and DataSourceAzure.py needs to be switched
-to use these.
-
-Resolves: rhbz#1434109
-
-X-downstream-only: yes
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-Signed-off-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
----
- cloudinit/sources/DataSourceAzure.py | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index cee630f7..553b5a7e 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -296,7 +296,7 @@ def get_hostname(hostname_command='hostname'):
- 
- 
- def set_hostname(hostname, hostname_command='hostname'):
--    subp.subp([hostname_command, hostname])
-+    util.subp(['hostnamectl', 'set-hostname', str(hostname)])
- 
- 
- @azure_ds_telemetry_reporter
--- 
-2.27.0
-
diff --git a/SOURCES/0005-Remove-race-condition-between-cloud-init-and-Network.patch b/SOURCES/0005-Remove-race-condition-between-cloud-init-and-Network.patch
new file mode 100644
index 0000000..478e5ab
--- /dev/null
+++ b/SOURCES/0005-Remove-race-condition-between-cloud-init-and-Network.patch
@@ -0,0 +1,148 @@
+From 386f0a82bfdfd62e506bf4251c17263260d3250a Mon Sep 17 00:00:00 2001
+From: Eduardo Otubo <otubo@redhat.com>
+Date: Fri, 7 May 2021 13:36:14 +0200
+Subject: Remove race condition between cloud-init and NetworkManager
+
+Message-id: <20200302104635.11648-1-otubo@redhat.com>
+Patchwork-id: 94098
+O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Remove race condition between cloud-init and NetworkManager
+Bugzilla: 1807797
+RH-Acked-by: Cathy Avery <cavery@redhat.com>
+RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
+
+BZ: 1748015
+BRANCH: rhel7/master-18.5
+BREW: 26924611
+
+BZ: 1807797
+BRANCH: rhel820/master-18.5
+BREW: 26924957
+
+cloud-init service is set to start before NetworkManager service starts,
+but this does not avoid a race condition between them. NetworkManager
+starts before cloud-init can write `dns=none' to the file:
+/etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager
+doesn't read the configuration and erases all resolv.conf values upon
+shutdown. On the next reboot neither cloud-init or NetworkManager will
+write anything to resolv.conf, leaving it blank.
+
+This patch introduces a NM reload (try-restart) at the end of cloud-init
+start up so it won't erase resolv.conf upon first shutdown.
+
+x-downstream-only: yes
+resolves: rhbz#1748015, rhbz#1807797 and rhbz#1804780
+
+Signed-off-by: Eduardo Otubo <otubo@redhat.com>
+Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
+
+This commit is a squash and also includes the folloowing commits:
+
+commit 316a17b7c02a87fa9b2981535be0b20d165adc46
+Author: Eduardo Otubo <otubo@redhat.com>
+Date:   Mon Jun 1 11:58:06 2020 +0200
+
+    Make cloud-init.service execute after network is up
+
+    RH-Author: Eduardo Otubo <otubo@redhat.com>
+    Message-id: <20200526090804.2047-1-otubo@redhat.com>
+    Patchwork-id: 96809
+    O-Subject: [RHEL-8.2.1 cloud-init PATCH] Make cloud-init.service execute after network is up
+    Bugzilla: 1803928
+    RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+    RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
+
+    cloud-init.service needs to wait until network is fully up before
+    continuing executing and configuring its service.
+
+    Signed-off-by: Eduardo Otubo <otubo@redhat.com>
+
+    x-downstream-only: yes
+    Resolves: rhbz#1831646
+    Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
+
+commit 0422ba0e773d1a8257a3f2bf3db05f3bc7917eb7
+Author: Eduardo Otubo <otubo@redhat.com>
+Date:   Thu May 28 08:44:08 2020 +0200
+
+    Remove race condition between cloud-init and NetworkManager
+
+    RH-Author: Eduardo Otubo <otubo@redhat.com>
+    Message-id: <20200327121911.17699-1-otubo@redhat.com>
+    Patchwork-id: 94453
+    O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCHv2] Remove race condition between cloud-init and NetworkManager
+    Bugzilla: 1840648
+    RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+    RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
+    RH-Acked-by: Cathy Avery <cavery@redhat.com>
+
+    cloud-init service is set to start before NetworkManager service starts,
+    but this does not avoid a race condition between them. NetworkManager
+    starts before cloud-init can write `dns=none' to the file:
+    /etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager
+    doesn't read the configuration and erases all resolv.conf values upon
+    shutdown. On the next reboot neither cloud-init or NetworkManager will
+    write anything to resolv.conf, leaving it blank.
+
+    This patch introduces a NM reload (try-reload-or-restart) at the end of cloud-init
+    start up so it won't erase resolv.conf upon first shutdown.
+
+    x-downstream-only: yes
+
+    Signed-off-by: Eduardo Otubo otubo@redhat.com
+    Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
+
+commit e0b48a936433faea7f56dbc29dda35acf7d375f7
+Author: Eduardo Otubo <otubo@redhat.com>
+Date:   Thu May 28 08:44:06 2020 +0200
+
+    Enable ssh_deletekeys by default
+
+    RH-Author: Eduardo Otubo <otubo@redhat.com>
+    Message-id: <20200317091705.15715-1-otubo@redhat.com>
+    Patchwork-id: 94365
+    O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Enable ssh_deletekeys by default
+    Bugzilla: 1814152
+    RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
+    RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+
+    The configuration option ssh_deletekeys will trigger the generation
+    of new ssh keys for every new instance deployed.
+
+    x-downstream-only: yes
+    resolves: rhbz#1814152
+
+    Signed-off-by: Eduardo Otubo <otubo@redhat.com>
+    Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
+---
+ rhel/cloud.cfg                  | 2 +-
+ rhel/systemd/cloud-init.service | 1 +
+ 2 files changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
+index 82e8bf62..9ecba215 100644
+--- a/rhel/cloud.cfg
++++ b/rhel/cloud.cfg
+@@ -6,7 +6,7 @@ ssh_pwauth:   0
+ 
+ mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
+ resize_rootfs_tmp: /dev
+-ssh_deletekeys:   0
++ssh_deletekeys:   1
+ ssh_genkeytypes:  ~
+ syslog_fix_perms: ~
+ disable_vmware_customization: false
+diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service
+index d0023a05..0b3d796d 100644
+--- a/rhel/systemd/cloud-init.service
++++ b/rhel/systemd/cloud-init.service
+@@ -5,6 +5,7 @@ Wants=sshd-keygen.service
+ Wants=sshd.service
+ After=cloud-init-local.service
+ After=NetworkManager.service network.service
++After=NetworkManager-wait-online.service
+ Before=network-online.target
+ Before=sshd-keygen.service
+ Before=sshd.service
+-- 
+2.31.1
+
diff --git a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch b/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
deleted file mode 100644
index 6276255..0000000
--- a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
+++ /dev/null
@@ -1,65 +0,0 @@
-From dfea0490b899804761fbd7aa23822783d7c36ec5 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Fri, 7 May 2021 13:36:13 +0200
-Subject: include 'NOZEROCONF=yes' in /etc/sysconfig/network
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-Message-id: <20190320114559.23708-1-otubo@redhat.com>
-Patchwork-id: 84937
-O-Subject: [RHEL-7.7 cloud-init PATCH] include 'NOZEROCONF=yes' in /etc/sysconfig/network
-Bugzilla: 1653131
-RH-Acked-by: Cathy Avery <cavery@redhat.com>
-RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-
-The option NOZEROCONF=yes is not included by default in
-/etc/sysconfig/network, which is required by Overcloud instances. The
-patch also includes tests for the modifications.
-
-X-downstream-only: yes
-Resolves: rhbz#1653131
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
----
- cloudinit/net/sysconfig.py  | 11 ++++++++++-
- tests/unittests/test_net.py |  1 -
- 2 files changed, 10 insertions(+), 2 deletions(-)
-
-diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index 3d276666..d5440998 100644
---- a/cloudinit/net/sysconfig.py
-+++ b/cloudinit/net/sysconfig.py
-@@ -925,7 +925,16 @@ class Renderer(renderer.Renderer):
-         # Distros configuring /etc/sysconfig/network as a file e.g. Centos
-         if sysconfig_path.endswith('network'):
-             util.ensure_dir(os.path.dirname(sysconfig_path))
--            netcfg = [_make_header(), 'NETWORKING=yes']
-+            netcfg = []
-+            for line in util.load_file(sysconfig_path, quiet=True).split('\n'):
-+                if 'cloud-init' in line:
-+                    break
-+                if not line.startswith(('NETWORKING=',
-+                                        'IPV6_AUTOCONF=',
-+                                        'NETWORKING_IPV6=')):
-+                    netcfg.append(line)
-+            # Now generate the cloud-init portion of sysconfig/network
-+            netcfg.extend([_make_header(), 'NETWORKING=yes'])
-             if network_state.use_ipv6:
-                 netcfg.append('NETWORKING_IPV6=yes')
-                 netcfg.append('IPV6_AUTOCONF=no')
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 4ea0e597..c67b5fcc 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -1729,7 +1729,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
-                 BOOTPROTO=none
-                 DEVICE=bond0
-                 DHCPV6C=yes
--                IPV6_AUTOCONF=no
-                 IPV6INIT=yes
-                 MACADDR=aa:bb:cc:dd:ee:ff
-                 ONBOOT=yes
--- 
-2.27.0
-
diff --git a/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch b/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
new file mode 100644
index 0000000..e596836
--- /dev/null
+++ b/SOURCES/0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
@@ -0,0 +1,65 @@
+From b545a0cbabe8924d048b7172b30e7aad59ed32d5 Mon Sep 17 00:00:00 2001
+From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date: Thu, 20 May 2021 08:53:55 +0200
+Subject: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in
+ cloud.cfg
+
+RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+RH-MergeRequest: 10: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in cloud.cfg
+RH-Commit: [1/1] 6da989423b9b6e017afbac2f1af3649b0487310f
+RH-Bugzilla: 1957532
+RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
+RH-Acked-by: Cathy Avery <cavery@redhat.com>
+RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
+
+Currently genkeytypes in cloud.cfg is set to None, so together with
+ssh_deletekeys=1 cloudinit on first boot it will just delete the existing
+keys and not generate new ones.
+
+Just removing that property in cloud.cfg is not enough, because
+settings.py provides another empty default value that will be used
+instead, resulting to no key generated even when the property is not defined.
+
+Removing genkeytypes also in settings.py will default to GENERATE_KEY_NAMES,
+but since we want only 'rsa', 'ecdsa' and 'ed25519', add back genkeytypes in
+cloud.cfg with the above defaults.
+
+Also remove ssh_deletekeys in settings.py as we always need
+to 1 (and it also defaults to 1).
+
+Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+---
+ cloudinit/settings.py | 2 --
+ rhel/cloud.cfg        | 2 +-
+ 2 files changed, 1 insertion(+), 3 deletions(-)
+
+diff --git a/cloudinit/settings.py b/cloudinit/settings.py
+index 3c2145e9..71672e10 100644
+--- a/cloudinit/settings.py
++++ b/cloudinit/settings.py
+@@ -52,8 +52,6 @@ CFG_BUILTIN = {
+     "def_log_file_mode": 0o600,
+     "log_cfgs": [],
+     "mount_default_fields": [None, None, "auto", "defaults,nofail", "0", "2"],
+-    "ssh_deletekeys": False,
+-    "ssh_genkeytypes": [],
+     "syslog_fix_perms": [],
+     "system_info": {
+         "paths": {
+diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
+index 9ecba215..cbee197a 100644
+--- a/rhel/cloud.cfg
++++ b/rhel/cloud.cfg
+@@ -7,7 +7,7 @@ ssh_pwauth:   0
+ mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
+ resize_rootfs_tmp: /dev
+ ssh_deletekeys:   1
+-ssh_genkeytypes:  ~
++ssh_genkeytypes:  ['rsa', 'ecdsa', 'ed25519']
+ syslog_fix_perms: ~
+ disable_vmware_customization: false
+ 
+-- 
+2.31.1
+
diff --git a/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch b/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch
deleted file mode 100644
index 9c9e4cc..0000000
--- a/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch
+++ /dev/null
@@ -1,148 +0,0 @@
-From 24894dcf45a307f44e29dc5d5b2d864b75fd982c Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Fri, 7 May 2021 13:36:14 +0200
-Subject: Remove race condition between cloud-init and NetworkManager
-
-Message-id: <20200302104635.11648-1-otubo@redhat.com>
-Patchwork-id: 94098
-O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Remove race condition between cloud-init and NetworkManager
-Bugzilla: 1807797
-RH-Acked-by: Cathy Avery <cavery@redhat.com>
-RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
-
-BZ: 1748015
-BRANCH: rhel7/master-18.5
-BREW: 26924611
-
-BZ: 1807797
-BRANCH: rhel820/master-18.5
-BREW: 26924957
-
-cloud-init service is set to start before NetworkManager service starts,
-but this does not avoid a race condition between them. NetworkManager
-starts before cloud-init can write `dns=none' to the file:
-/etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager
-doesn't read the configuration and erases all resolv.conf values upon
-shutdown. On the next reboot neither cloud-init or NetworkManager will
-write anything to resolv.conf, leaving it blank.
-
-This patch introduces a NM reload (try-restart) at the end of cloud-init
-start up so it won't erase resolv.conf upon first shutdown.
-
-x-downstream-only: yes
-resolves: rhbz#1748015, rhbz#1807797 and rhbz#1804780
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
-
-This commit is a squash and also includes the folloowing commits:
-
-commit 316a17b7c02a87fa9b2981535be0b20d165adc46
-Author: Eduardo Otubo <otubo@redhat.com>
-Date:   Mon Jun 1 11:58:06 2020 +0200
-
-    Make cloud-init.service execute after network is up
-
-    RH-Author: Eduardo Otubo <otubo@redhat.com>
-    Message-id: <20200526090804.2047-1-otubo@redhat.com>
-    Patchwork-id: 96809
-    O-Subject: [RHEL-8.2.1 cloud-init PATCH] Make cloud-init.service execute after network is up
-    Bugzilla: 1803928
-    RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-    RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
-
-    cloud-init.service needs to wait until network is fully up before
-    continuing executing and configuring its service.
-
-    Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-
-    x-downstream-only: yes
-    Resolves: rhbz#1831646
-    Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
-
-commit 0422ba0e773d1a8257a3f2bf3db05f3bc7917eb7
-Author: Eduardo Otubo <otubo@redhat.com>
-Date:   Thu May 28 08:44:08 2020 +0200
-
-    Remove race condition between cloud-init and NetworkManager
-
-    RH-Author: Eduardo Otubo <otubo@redhat.com>
-    Message-id: <20200327121911.17699-1-otubo@redhat.com>
-    Patchwork-id: 94453
-    O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCHv2] Remove race condition between cloud-init and NetworkManager
-    Bugzilla: 1840648
-    RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-    RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
-    RH-Acked-by: Cathy Avery <cavery@redhat.com>
-
-    cloud-init service is set to start before NetworkManager service starts,
-    but this does not avoid a race condition between them. NetworkManager
-    starts before cloud-init can write `dns=none' to the file:
-    /etc/NetworkManager/conf.d/99-cloud-init.conf. This way NetworkManager
-    doesn't read the configuration and erases all resolv.conf values upon
-    shutdown. On the next reboot neither cloud-init or NetworkManager will
-    write anything to resolv.conf, leaving it blank.
-
-    This patch introduces a NM reload (try-reload-or-restart) at the end of cloud-init
-    start up so it won't erase resolv.conf upon first shutdown.
-
-    x-downstream-only: yes
-
-    Signed-off-by: Eduardo Otubo otubo@redhat.com
-    Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
-
-commit e0b48a936433faea7f56dbc29dda35acf7d375f7
-Author: Eduardo Otubo <otubo@redhat.com>
-Date:   Thu May 28 08:44:06 2020 +0200
-
-    Enable ssh_deletekeys by default
-
-    RH-Author: Eduardo Otubo <otubo@redhat.com>
-    Message-id: <20200317091705.15715-1-otubo@redhat.com>
-    Patchwork-id: 94365
-    O-Subject: [RHEL-7.9/RHEL-8.2.0 cloud-init PATCH] Enable ssh_deletekeys by default
-    Bugzilla: 1814152
-    RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
-    RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-
-    The configuration option ssh_deletekeys will trigger the generation
-    of new ssh keys for every new instance deployed.
-
-    x-downstream-only: yes
-    resolves: rhbz#1814152
-
-    Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-    Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
----
- rhel/cloud.cfg                  | 2 +-
- rhel/systemd/cloud-init.service | 1 +
- 2 files changed, 2 insertions(+), 1 deletion(-)
-
-diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
-index 82e8bf62..9ecba215 100644
---- a/rhel/cloud.cfg
-+++ b/rhel/cloud.cfg
-@@ -6,7 +6,7 @@ ssh_pwauth:   0
- 
- mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
- resize_rootfs_tmp: /dev
--ssh_deletekeys:   0
-+ssh_deletekeys:   1
- ssh_genkeytypes:  ~
- syslog_fix_perms: ~
- disable_vmware_customization: false
-diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service
-index d0023a05..0b3d796d 100644
---- a/rhel/systemd/cloud-init.service
-+++ b/rhel/systemd/cloud-init.service
-@@ -5,6 +5,7 @@ Wants=sshd-keygen.service
- Wants=sshd.service
- After=cloud-init-local.service
- After=NetworkManager.service network.service
-+After=NetworkManager-wait-online.service
- Before=network-online.target
- Before=sshd-keygen.service
- Before=sshd.service
--- 
-2.27.0
-
diff --git a/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch b/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch
deleted file mode 100644
index 38f08cc..0000000
--- a/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch
+++ /dev/null
@@ -1,496 +0,0 @@
-From b48dda73da94782d7ab0c455fa382d3a5ef3c419 Mon Sep 17 00:00:00 2001
-From: Daniel Watkins <oddbloke@ubuntu.com>
-Date: Mon, 8 Mar 2021 12:50:57 -0500
-Subject: net: exclude OVS internal interfaces in get_interfaces (#829)
-
-`get_interfaces` is used to in two ways, broadly: firstly, to determine
-the available interfaces when converting cloud network configuration
-formats to cloud-init's network configuration formats; and, secondly, to
-ensure that any interfaces which are specified in network configuration
-are (a) available, and (b) named correctly.  The first of these is
-unaffected by this commit, as no clouds support Open vSwitch
-configuration in their network configuration formats.
-
-For the second, we check that MAC addresses of physical devices are
-unique.  In some OVS configurations, there are OVS-created devices which
-have duplicate MAC addresses, either with each other or with physical
-devices.  As these interfaces are created by OVS, we can be confident
-that (a) they will be available when appropriate, and (b) that OVS will
-name them correctly.  As such, this commit excludes any OVS-internal
-interfaces from the set of interfaces returned by `get_interfaces`.
-
-LP: #1912844
----
- cloudinit/net/__init__.py                     |  62 +++++++++
- cloudinit/net/tests/test_init.py              | 119 ++++++++++++++++++
- .../sources/helpers/tests/test_openstack.py   |   5 +
- cloudinit/sources/tests/test_oracle.py        |   4 +
- .../integration_tests/bugs/test_lp1912844.py  | 103 +++++++++++++++
- .../test_datasource/test_configdrive.py       |   8 ++
- tests/unittests/test_net.py                   |  20 +++
- 7 files changed, 321 insertions(+)
- create mode 100644 tests/integration_tests/bugs/test_lp1912844.py
-
-diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
-index de65e7af..385b7bcc 100644
---- a/cloudinit/net/__init__.py
-+++ b/cloudinit/net/__init__.py
-@@ -6,6 +6,7 @@
- # This file is part of cloud-init. See LICENSE file for license information.
- 
- import errno
-+import functools
- import ipaddress
- import logging
- import os
-@@ -19,6 +20,19 @@ from cloudinit.url_helper import UrlError, readurl
- LOG = logging.getLogger(__name__)
- SYS_CLASS_NET = "/sys/class/net/"
- DEFAULT_PRIMARY_INTERFACE = 'eth0'
-+OVS_INTERNAL_INTERFACE_LOOKUP_CMD = [
-+    "ovs-vsctl",
-+    "--format",
-+    "csv",
-+    "--no-headings",
-+    "--timeout",
-+    "10",
-+    "--columns",
-+    "name",
-+    "find",
-+    "interface",
-+    "type=internal",
-+]
- 
- 
- def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
-@@ -133,6 +147,52 @@ def master_is_openvswitch(devname):
-     return os.path.exists(ovs_path)
- 
- 
-+@functools.lru_cache(maxsize=None)
-+def openvswitch_is_installed() -> bool:
-+    """Return a bool indicating if Open vSwitch is installed in the system."""
-+    ret = bool(subp.which("ovs-vsctl"))
-+    if not ret:
-+        LOG.debug(
-+            "ovs-vsctl not in PATH; not detecting Open vSwitch interfaces"
-+        )
-+    return ret
-+
-+
-+@functools.lru_cache(maxsize=None)
-+def get_ovs_internal_interfaces() -> list:
-+    """Return a list of the names of OVS internal interfaces on the system.
-+
-+    These will all be strings, and are used to exclude OVS-specific interface
-+    from cloud-init's network configuration handling.
-+    """
-+    try:
-+        out, _err = subp.subp(OVS_INTERNAL_INTERFACE_LOOKUP_CMD)
-+    except subp.ProcessExecutionError as exc:
-+        if "database connection failed" in exc.stderr:
-+            LOG.info(
-+                "Open vSwitch is not yet up; no interfaces will be detected as"
-+                " OVS-internal"
-+            )
-+            return []
-+        raise
-+    else:
-+        return out.splitlines()
-+
-+
-+def is_openvswitch_internal_interface(devname: str) -> bool:
-+    """Returns True if this is an OVS internal interface.
-+
-+    If OVS is not installed or not yet running, this will return False.
-+    """
-+    if not openvswitch_is_installed():
-+        return False
-+    ovs_bridges = get_ovs_internal_interfaces()
-+    if devname in ovs_bridges:
-+        LOG.debug("Detected %s as an OVS interface", devname)
-+        return True
-+    return False
-+
-+
- def is_netfailover(devname, driver=None):
-     """ netfailover driver uses 3 nics, master, primary and standby.
-         this returns True if the device is either the primary or standby
-@@ -884,6 +944,8 @@ def get_interfaces(blacklist_drivers=None) -> list:
-         # skip nics that have no mac (00:00....)
-         if name != 'lo' and mac == zero_mac[:len(mac)]:
-             continue
-+        if is_openvswitch_internal_interface(name):
-+            continue
-         # skip nics that have drivers blacklisted
-         driver = device_driver(name)
-         if driver in blacklist_drivers:
-diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
-index 0535387a..946f8ee2 100644
---- a/cloudinit/net/tests/test_init.py
-+++ b/cloudinit/net/tests/test_init.py
-@@ -391,6 +391,10 @@ class TestGetDeviceList(CiTestCase):
-         self.assertCountEqual(['eth0', 'eth1'], net.get_devicelist())
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False),
-+)
- class TestGetInterfaceMAC(CiTestCase):
- 
-     def setUp(self):
-@@ -1224,6 +1228,121 @@ class TestNetFailOver(CiTestCase):
-         self.assertFalse(net.is_netfailover(devname, driver))
- 
- 
-+class TestOpenvswitchIsInstalled:
-+    """Test cloudinit.net.openvswitch_is_installed.
-+
-+    Uses the ``clear_lru_cache`` local autouse fixture to allow us to test
-+    despite the ``lru_cache`` decorator on the unit under test.
-+    """
-+
-+    @pytest.fixture(autouse=True)
-+    def clear_lru_cache(self):
-+        net.openvswitch_is_installed.cache_clear()
-+
-+    @pytest.mark.parametrize(
-+        "expected,which_return", [(True, "/some/path"), (False, None)]
-+    )
-+    @mock.patch("cloudinit.net.subp.which")
-+    def test_mirrors_which_result(self, m_which, expected, which_return):
-+        m_which.return_value = which_return
-+        assert expected == net.openvswitch_is_installed()
-+
-+    @mock.patch("cloudinit.net.subp.which")
-+    def test_only_calls_which_once(self, m_which):
-+        net.openvswitch_is_installed()
-+        net.openvswitch_is_installed()
-+        assert 1 == m_which.call_count
-+
-+
-+@mock.patch("cloudinit.net.subp.subp", return_value=("", ""))
-+class TestGetOVSInternalInterfaces:
-+    """Test cloudinit.net.get_ovs_internal_interfaces.
-+
-+    Uses the ``clear_lru_cache`` local autouse fixture to allow us to test
-+    despite the ``lru_cache`` decorator on the unit under test.
-+    """
-+    @pytest.fixture(autouse=True)
-+    def clear_lru_cache(self):
-+        net.get_ovs_internal_interfaces.cache_clear()
-+
-+    def test_command_used(self, m_subp):
-+        """Test we use the correct command when we call subp"""
-+        net.get_ovs_internal_interfaces()
-+
-+        assert [
-+            mock.call(net.OVS_INTERNAL_INTERFACE_LOOKUP_CMD)
-+        ] == m_subp.call_args_list
-+
-+    def test_subp_contents_split_and_returned(self, m_subp):
-+        """Test that the command output is appropriately mangled."""
-+        stdout = "iface1\niface2\niface3\n"
-+        m_subp.return_value = (stdout, "")
-+
-+        assert [
-+            "iface1",
-+            "iface2",
-+            "iface3",
-+        ] == net.get_ovs_internal_interfaces()
-+
-+    def test_database_connection_error_handled_gracefully(self, m_subp):
-+        """Test that the error indicating OVS is down is handled gracefully."""
-+        m_subp.side_effect = ProcessExecutionError(
-+            stderr="database connection failed"
-+        )
-+
-+        assert [] == net.get_ovs_internal_interfaces()
-+
-+    def test_other_errors_raised(self, m_subp):
-+        """Test that only database connection errors are handled."""
-+        m_subp.side_effect = ProcessExecutionError()
-+
-+        with pytest.raises(ProcessExecutionError):
-+            net.get_ovs_internal_interfaces()
-+
-+    def test_only_runs_once(self, m_subp):
-+        """Test that we cache the value."""
-+        net.get_ovs_internal_interfaces()
-+        net.get_ovs_internal_interfaces()
-+
-+        assert 1 == m_subp.call_count
-+
-+
-+@mock.patch("cloudinit.net.get_ovs_internal_interfaces")
-+@mock.patch("cloudinit.net.openvswitch_is_installed")
-+class TestIsOpenVSwitchInternalInterface:
-+    def test_false_if_ovs_not_installed(
-+        self, m_openvswitch_is_installed, _m_get_ovs_internal_interfaces
-+    ):
-+        """Test that OVS' absence returns False."""
-+        m_openvswitch_is_installed.return_value = False
-+
-+        assert not net.is_openvswitch_internal_interface("devname")
-+
-+    @pytest.mark.parametrize(
-+        "detected_interfaces,devname,expected_return",
-+        [
-+            ([], "devname", False),
-+            (["notdevname"], "devname", False),
-+            (["devname"], "devname", True),
-+            (["some", "other", "devices", "and", "ours"], "ours", True),
-+        ],
-+    )
-+    def test_return_value_based_on_detected_interfaces(
-+        self,
-+        m_openvswitch_is_installed,
-+        m_get_ovs_internal_interfaces,
-+        detected_interfaces,
-+        devname,
-+        expected_return,
-+    ):
-+        """Test that the detected interfaces are used correctly."""
-+        m_openvswitch_is_installed.return_value = True
-+        m_get_ovs_internal_interfaces.return_value = detected_interfaces
-+        assert expected_return == net.is_openvswitch_internal_interface(
-+            devname
-+        )
-+
-+
- class TestIsIpAddress:
-     """Tests for net.is_ip_address.
- 
-diff --git a/cloudinit/sources/helpers/tests/test_openstack.py b/cloudinit/sources/helpers/tests/test_openstack.py
-index 2bde1e3f..95fb9743 100644
---- a/cloudinit/sources/helpers/tests/test_openstack.py
-+++ b/cloudinit/sources/helpers/tests/test_openstack.py
-@@ -1,10 +1,15 @@
- # This file is part of cloud-init. See LICENSE file for license information.
- # ./cloudinit/sources/helpers/tests/test_openstack.py
-+from unittest import mock
- 
- from cloudinit.sources.helpers import openstack
- from cloudinit.tests import helpers as test_helpers
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestConvertNetJson(test_helpers.CiTestCase):
- 
-     def test_phy_types(self):
-diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
-index a7bbdfd9..dcf33b9b 100644
---- a/cloudinit/sources/tests/test_oracle.py
-+++ b/cloudinit/sources/tests/test_oracle.py
-@@ -173,6 +173,10 @@ class TestIsPlatformViable(test_helpers.CiTestCase):
-         m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestNetworkConfigFromOpcImds:
-     def test_no_secondary_nics_does_not_mutate_input(self, oracle_ds):
-         oracle_ds._vnics_data = [{}]
-diff --git a/tests/integration_tests/bugs/test_lp1912844.py b/tests/integration_tests/bugs/test_lp1912844.py
-new file mode 100644
-index 00000000..efafae50
---- /dev/null
-+++ b/tests/integration_tests/bugs/test_lp1912844.py
-@@ -0,0 +1,103 @@
-+"""Integration test for LP: #1912844
-+
-+cloud-init should ignore OVS-internal interfaces when performing its own
-+interface determination: these interfaces are handled fully by OVS, so
-+cloud-init should never need to touch them.
-+
-+This test is a semi-synthetic reproducer for the bug.  It uses a similar
-+network configuration, tweaked slightly to DHCP in a way that will succeed even
-+on "failed" boots.  The exact bug doesn't reproduce with the NoCloud
-+datasource, because it runs at init-local time (whereas the MAAS datasource,
-+from the report, runs only at init (network) time): this means that the
-+networking code runs before OVS creates its interfaces (which happens after
-+init-local but, of course, before networking is up), and so doesn't generate
-+the traceback that they cause.  We work around this by calling
-+``get_interfaces_by_mac` directly in the test code.
-+"""
-+import pytest
-+
-+from tests.integration_tests import random_mac_address
-+
-+MAC_ADDRESS = random_mac_address()
-+
-+NETWORK_CONFIG = """\
-+bonds:
-+    bond0:
-+        interfaces:
-+            - enp5s0
-+        macaddress: {0}
-+        mtu: 1500
-+bridges:
-+        ovs-br:
-+            interfaces:
-+            - bond0
-+            macaddress: {0}
-+            mtu: 1500
-+            openvswitch: {{}}
-+            dhcp4: true
-+ethernets:
-+    enp5s0:
-+      mtu: 1500
-+      set-name: enp5s0
-+      match:
-+          macaddress: {0}
-+version: 2
-+vlans:
-+  ovs-br.100:
-+    id: 100
-+    link: ovs-br
-+    mtu: 1500
-+  ovs-br.200:
-+    id: 200
-+    link: ovs-br
-+    mtu: 1500
-+""".format(MAC_ADDRESS)
-+
-+
-+SETUP_USER_DATA = """\
-+#cloud-config
-+packages:
-+- openvswitch-switch
-+"""
-+
-+
-+@pytest.fixture
-+def ovs_enabled_session_cloud(session_cloud):
-+    """A session_cloud wrapper, to use an OVS-enabled image for tests.
-+
-+    This implementation is complicated by wanting to use ``session_cloud``s
-+    snapshot cleanup/retention logic, to avoid having to reimplement that here.
-+    """
-+    old_snapshot_id = session_cloud.snapshot_id
-+    with session_cloud.launch(
-+        user_data=SETUP_USER_DATA,
-+    ) as instance:
-+        instance.instance.clean()
-+        session_cloud.snapshot_id = instance.snapshot()
-+
-+    yield session_cloud
-+
-+    try:
-+        session_cloud.delete_snapshot()
-+    finally:
-+        session_cloud.snapshot_id = old_snapshot_id
-+
-+
-+@pytest.mark.lxd_vm
-+def test_get_interfaces_by_mac_doesnt_traceback(ovs_enabled_session_cloud):
-+    """Launch our OVS-enabled image and confirm the bug doesn't reproduce."""
-+    launch_kwargs = {
-+        "config_dict": {
-+            "user.network-config": NETWORK_CONFIG,
-+            "volatile.eth0.hwaddr": MAC_ADDRESS,
-+        },
-+    }
-+    with ovs_enabled_session_cloud.launch(
-+        launch_kwargs=launch_kwargs,
-+    ) as client:
-+        result = client.execute(
-+            "python3 -c"
-+            "'from cloudinit.net import get_interfaces_by_mac;"
-+            "get_interfaces_by_mac()'"
-+        )
-+        assert result.ok
-diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
-index 6f830cc6..2e2b7847 100644
---- a/tests/unittests/test_datasource/test_configdrive.py
-+++ b/tests/unittests/test_datasource/test_configdrive.py
-@@ -494,6 +494,10 @@ class TestConfigDriveDataSource(CiTestCase):
-         self.assertEqual('config-disk (/dev/anything)', cfg_ds.subplatform)
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestNetJson(CiTestCase):
-     def setUp(self):
-         super(TestNetJson, self).setUp()
-@@ -654,6 +658,10 @@ class TestNetJson(CiTestCase):
-             self.assertEqual(out_data, conv_data)
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestConvertNetworkData(CiTestCase):
- 
-     with_logs = True
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index c67b5fcc..14d3462f 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -2908,6 +2908,10 @@ iface eth1 inet dhcp
-         self.assertEqual(0, mock_settle.call_count)
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestRhelSysConfigRendering(CiTestCase):
- 
-     with_logs = True
-@@ -3592,6 +3596,10 @@ USERCTL=no
-                 expected, self._render_and_read(network_config=v2data))
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestOpenSuseSysConfigRendering(CiTestCase):
- 
-     with_logs = True
-@@ -5009,6 +5017,10 @@ class TestNetRenderers(CiTestCase):
-             self.assertTrue(result)
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestGetInterfaces(CiTestCase):
-     _data = {'bonds': ['bond1'],
-              'bridges': ['bridge1'],
-@@ -5158,6 +5170,10 @@ class TestInterfaceHasOwnMac(CiTestCase):
-         self.assertFalse(interface_has_own_mac("eth0"))
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestGetInterfacesByMac(CiTestCase):
-     _data = {'bonds': ['bond1'],
-              'bridges': ['bridge1'],
-@@ -5314,6 +5330,10 @@ class TestInterfacesSorting(CiTestCase):
-             ['enp0s3', 'enp0s8', 'enp0s13', 'enp1s2', 'enp2s0', 'enp2s3'])
- 
- 
-+@mock.patch(
-+    "cloudinit.net.is_openvswitch_internal_interface",
-+    mock.Mock(return_value=False)
-+)
- class TestGetIBHwaddrsByInterface(CiTestCase):
- 
-     _ib_addr = '80:00:00:28:fe:80:00:00:00:00:00:00:00:11:22:03:00:33:44:56'
--- 
-2.27.0
-
diff --git a/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch b/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch
deleted file mode 100644
index 0d474bc..0000000
--- a/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch
+++ /dev/null
@@ -1,87 +0,0 @@
-From bec5fb60ffae3d1137c7261e5571c2751c5dda25 Mon Sep 17 00:00:00 2001
-From: James Falcon <TheRealFalcon@users.noreply.github.com>
-Date: Mon, 8 Mar 2021 14:09:47 -0600
-Subject: Fix requiring device-number on EC2 derivatives (#836)
-
-#342 (70dbccbb) introduced the ability to determine route-metrics based on
-the `device-number` provided by the EC2 IMDS. Not all datasources that
-subclass EC2 will have this attribute, so allow the old behavior if
-`device-number` is not present.
-
-LP: #1917875
----
- cloudinit/sources/DataSourceEc2.py            |  3 +-
- .../unittests/test_datasource/test_aliyun.py  | 30 +++++++++++++++++++
- 2 files changed, 32 insertions(+), 1 deletion(-)
-
-diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
-index 1930a509..a2105dc7 100644
---- a/cloudinit/sources/DataSourceEc2.py
-+++ b/cloudinit/sources/DataSourceEc2.py
-@@ -765,13 +765,14 @@ def convert_ec2_metadata_network_config(
-         netcfg['ethernets'][nic_name] = dev_config
-         return netcfg
-     # Apply network config for all nics and any secondary IPv4/v6 addresses
-+    nic_idx = 0
-     for mac, nic_name in sorted(macs_to_nics.items()):
-         nic_metadata = macs_metadata.get(mac)
-         if not nic_metadata:
-             continue  # Not a physical nic represented in metadata
-         # device-number is zero-indexed, we want it 1-indexed for the
-         # multiplication on the following line
--        nic_idx = int(nic_metadata['device-number']) + 1
-+        nic_idx = int(nic_metadata.get('device-number', nic_idx)) + 1
-         dhcp_override = {'route-metric': nic_idx * 100}
-         dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override,
-                       'dhcp6': False,
-diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py
-index eb2828d5..cab1ac2b 100644
---- a/tests/unittests/test_datasource/test_aliyun.py
-+++ b/tests/unittests/test_datasource/test_aliyun.py
-@@ -7,6 +7,7 @@ from unittest import mock
- 
- from cloudinit import helpers
- from cloudinit.sources import DataSourceAliYun as ay
-+from cloudinit.sources.DataSourceEc2 import convert_ec2_metadata_network_config
- from cloudinit.tests import helpers as test_helpers
- 
- DEFAULT_METADATA = {
-@@ -183,6 +184,35 @@ class TestAliYunDatasource(test_helpers.HttprettyTestCase):
-         self.assertEqual(ay.parse_public_keys(public_keys),
-                          public_keys['key-pair-0']['openssh-key'])
- 
-+    def test_route_metric_calculated_without_device_number(self):
-+        """Test that route-metric code works without `device-number`
-+
-+        `device-number` is part of EC2 metadata, but not supported on aliyun.
-+        Attempting to access it will raise a KeyError.
-+
-+        LP: #1917875
-+        """
-+        netcfg = convert_ec2_metadata_network_config(
-+            {"interfaces": {"macs": {
-+                "06:17:04:d7:26:09": {
-+                    "interface-id": "eni-e44ef49e",
-+                },
-+                "06:17:04:d7:26:08": {
-+                    "interface-id": "eni-e44ef49f",
-+                }
-+            }}},
-+            macs_to_nics={
-+                '06:17:04:d7:26:09': 'eth0',
-+                '06:17:04:d7:26:08': 'eth1',
-+            }
-+        )
-+
-+        met0 = netcfg['ethernets']['eth0']['dhcp4-overrides']['route-metric']
-+        met1 = netcfg['ethernets']['eth1']['dhcp4-overrides']['route-metric']
-+
-+        # route-metric numbers should be 100 apart
-+        assert 100 == abs(met0 - met1)
-+
- 
- class TestIsAliYun(test_helpers.CiTestCase):
-     ALIYUN_PRODUCT = 'Alibaba Cloud ECS'
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch b/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch
deleted file mode 100644
index 9dd373f..0000000
--- a/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch
+++ /dev/null
@@ -1,295 +0,0 @@
-From 2a2a5cdec0de0b96d503f9357c1641043574f90a Mon Sep 17 00:00:00 2001
-From: Thomas Stringer <thstring@microsoft.com>
-Date: Wed, 3 Mar 2021 11:07:43 -0500
-Subject: [PATCH 1/7] Add flexibility to IMDS api-version (#793)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [1/7] 9aa42581c4ff175fb6f8f4a78d94cac9c9971062
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-Add flexibility to IMDS api-version by having both a desired IMDS
-api-version and a minimum api-version. The desired api-version will
-be used first, and if that fails it will fall back to the minimum
-api-version.
----
- cloudinit/sources/DataSourceAzure.py          | 113 ++++++++++++++----
- tests/unittests/test_datasource/test_azure.py |  42 ++++++-
- 2 files changed, 129 insertions(+), 26 deletions(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index 553b5a7e..de1452ce 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -78,17 +78,15 @@ AGENT_SEED_DIR = '/var/lib/waagent'
- # In the event where the IMDS primary server is not
- # available, it takes 1s to fallback to the secondary one
- IMDS_TIMEOUT_IN_SECONDS = 2
--IMDS_URL = "http://169.254.169.254/metadata/"
--IMDS_VER = "2019-06-01"
--IMDS_VER_PARAM = "api-version={}".format(IMDS_VER)
-+IMDS_URL = "http://169.254.169.254/metadata"
-+IMDS_VER_MIN = "2019-06-01"
-+IMDS_VER_WANT = "2020-09-01"
- 
- 
- class metadata_type(Enum):
--    compute = "{}instance?{}".format(IMDS_URL, IMDS_VER_PARAM)
--    network = "{}instance/network?{}".format(IMDS_URL,
--                                             IMDS_VER_PARAM)
--    reprovisiondata = "{}reprovisiondata?{}".format(IMDS_URL,
--                                                    IMDS_VER_PARAM)
-+    compute = "{}/instance".format(IMDS_URL)
-+    network = "{}/instance/network".format(IMDS_URL)
-+    reprovisiondata = "{}/reprovisiondata".format(IMDS_URL)
- 
- 
- PLATFORM_ENTROPY_SOURCE = "/sys/firmware/acpi/tables/OEM0"
-@@ -349,6 +347,8 @@ class DataSourceAzure(sources.DataSource):
-         self.update_events['network'].add(EventType.BOOT)
-         self._ephemeral_dhcp_ctx = None
- 
-+        self.failed_desired_api_version = False
-+
-     def __str__(self):
-         root = sources.DataSource.__str__(self)
-         return "%s [seed=%s]" % (root, self.seed)
-@@ -520,8 +520,10 @@ class DataSourceAzure(sources.DataSource):
-                     self._wait_for_all_nics_ready()
-                 ret = self._reprovision()
- 
--            imds_md = get_metadata_from_imds(
--                self.fallback_interface, retries=10)
-+            imds_md = self.get_imds_data_with_api_fallback(
-+                self.fallback_interface,
-+                retries=10
-+            )
-             (md, userdata_raw, cfg, files) = ret
-             self.seed = cdev
-             crawled_data.update({
-@@ -652,6 +654,57 @@ class DataSourceAzure(sources.DataSource):
-             self.ds_cfg['data_dir'], crawled_data['files'], dirmode=0o700)
-         return True
- 
-+    @azure_ds_telemetry_reporter
-+    def get_imds_data_with_api_fallback(
-+            self,
-+            fallback_nic,
-+            retries,
-+            md_type=metadata_type.compute):
-+        """
-+        Wrapper for get_metadata_from_imds so that we can have flexibility
-+        in which IMDS api-version we use. If a particular instance of IMDS
-+        does not have the api version that is desired, we want to make
-+        this fault tolerant and fall back to a good known minimum api
-+        version.
-+        """
-+
-+        if not self.failed_desired_api_version:
-+            for _ in range(retries):
-+                try:
-+                    LOG.info(
-+                        "Attempting IMDS api-version: %s",
-+                        IMDS_VER_WANT
-+                    )
-+                    return get_metadata_from_imds(
-+                        fallback_nic=fallback_nic,
-+                        retries=0,
-+                        md_type=md_type,
-+                        api_version=IMDS_VER_WANT
-+                    )
-+                except UrlError as err:
-+                    LOG.info(
-+                        "UrlError with IMDS api-version: %s",
-+                        IMDS_VER_WANT
-+                    )
-+                    if err.code == 400:
-+                        log_msg = "Fall back to IMDS api-version: {}".format(
-+                            IMDS_VER_MIN
-+                        )
-+                        report_diagnostic_event(
-+                            log_msg,
-+                            logger_func=LOG.info
-+                        )
-+                        self.failed_desired_api_version = True
-+                        break
-+
-+        LOG.info("Using IMDS api-version: %s", IMDS_VER_MIN)
-+        return get_metadata_from_imds(
-+            fallback_nic=fallback_nic,
-+            retries=retries,
-+            md_type=md_type,
-+            api_version=IMDS_VER_MIN
-+        )
-+
-     def device_name_to_device(self, name):
-         return self.ds_cfg['disk_aliases'].get(name)
- 
-@@ -880,10 +933,11 @@ class DataSourceAzure(sources.DataSource):
-         # primary nic is being attached first helps here. Otherwise each nic
-         # could add several seconds of delay.
-         try:
--            imds_md = get_metadata_from_imds(
-+            imds_md = self.get_imds_data_with_api_fallback(
-                 ifname,
-                 5,
--                metadata_type.network)
-+                metadata_type.network
-+            )
-         except Exception as e:
-             LOG.warning(
-                 "Failed to get network metadata using nic %s. Attempt to "
-@@ -1017,7 +1071,10 @@ class DataSourceAzure(sources.DataSource):
-     def _poll_imds(self):
-         """Poll IMDS for the new provisioning data until we get a valid
-         response. Then return the returned JSON object."""
--        url = metadata_type.reprovisiondata.value
-+        url = "{}?api-version={}".format(
-+            metadata_type.reprovisiondata.value,
-+            IMDS_VER_MIN
-+        )
-         headers = {"Metadata": "true"}
-         nl_sock = None
-         report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE))
-@@ -2059,7 +2116,8 @@ def _generate_network_config_from_fallback_config() -> dict:
- @azure_ds_telemetry_reporter
- def get_metadata_from_imds(fallback_nic,
-                            retries,
--                           md_type=metadata_type.compute):
-+                           md_type=metadata_type.compute,
-+                           api_version=IMDS_VER_MIN):
-     """Query Azure's instance metadata service, returning a dictionary.
- 
-     If network is not up, setup ephemeral dhcp on fallback_nic to talk to the
-@@ -2069,13 +2127,16 @@ def get_metadata_from_imds(fallback_nic,
-     @param fallback_nic: String. The name of the nic which requires active
-         network in order to query IMDS.
-     @param retries: The number of retries of the IMDS_URL.
-+    @param md_type: Metadata type for IMDS request.
-+    @param api_version: IMDS api-version to use in the request.
- 
-     @return: A dict of instance metadata containing compute and network
-         info.
-     """
-     kwargs = {'logfunc': LOG.debug,
-               'msg': 'Crawl of Azure Instance Metadata Service (IMDS)',
--              'func': _get_metadata_from_imds, 'args': (retries, md_type,)}
-+              'func': _get_metadata_from_imds,
-+              'args': (retries, md_type, api_version,)}
-     if net.is_up(fallback_nic):
-         return util.log_time(**kwargs)
-     else:
-@@ -2091,20 +2152,26 @@ def get_metadata_from_imds(fallback_nic,
- 
- 
- @azure_ds_telemetry_reporter
--def _get_metadata_from_imds(retries, md_type=metadata_type.compute):
--
--    url = md_type.value
-+def _get_metadata_from_imds(
-+        retries,
-+        md_type=metadata_type.compute,
-+        api_version=IMDS_VER_MIN):
-+    url = "{}?api-version={}".format(md_type.value, api_version)
-     headers = {"Metadata": "true"}
-     try:
-         response = readurl(
-             url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers,
-             retries=retries, exception_cb=retry_on_url_exc)
-     except Exception as e:
--        report_diagnostic_event(
--            'Ignoring IMDS instance metadata. '
--            'Get metadata from IMDS failed: %s' % e,
--            logger_func=LOG.warning)
--        return {}
-+        # pylint:disable=no-member
-+        if isinstance(e, UrlError) and e.code == 400:
-+            raise
-+        else:
-+            report_diagnostic_event(
-+                'Ignoring IMDS instance metadata. '
-+                'Get metadata from IMDS failed: %s' % e,
-+                logger_func=LOG.warning)
-+            return {}
-     try:
-         from json.decoder import JSONDecodeError
-         json_decode_error = JSONDecodeError
-diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
-index f597c723..dedebeb1 100644
---- a/tests/unittests/test_datasource/test_azure.py
-+++ b/tests/unittests/test_datasource/test_azure.py
-@@ -408,7 +408,9 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
- 
-     def setUp(self):
-         super(TestGetMetadataFromIMDS, self).setUp()
--        self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2019-06-01"
-+        self.network_md_url = "{}/instance?api-version=2019-06-01".format(
-+            dsaz.IMDS_URL
-+        )
- 
-     @mock.patch(MOCKPATH + 'readurl')
-     @mock.patch(MOCKPATH + 'EphemeralDHCPv4', autospec=True)
-@@ -518,7 +520,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
-         """Return empty dict when IMDS network metadata is absent."""
-         httpretty.register_uri(
-             httpretty.GET,
--            dsaz.IMDS_URL + 'instance?api-version=2017-12-01',
-+            dsaz.IMDS_URL + '/instance?api-version=2017-12-01',
-             body={}, status=404)
- 
-         m_net_is_up.return_value = True  # skips dhcp
-@@ -1877,6 +1879,40 @@ scbus-1 on xpt0 bus 0
-         ssh_keys = dsrc.get_public_ssh_keys()
-         self.assertEqual(ssh_keys, ['key2'])
- 
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_imds_api_version_wanted_nonexistent(
-+            self,
-+            m_get_metadata_from_imds):
-+        def get_metadata_from_imds_side_eff(*args, **kwargs):
-+            if kwargs['api_version'] == dsaz.IMDS_VER_WANT:
-+                raise url_helper.UrlError("No IMDS version", code=400)
-+            return NETWORK_METADATA
-+        m_get_metadata_from_imds.side_effect = get_metadata_from_imds_side_eff
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        dsrc = self._get_ds(data)
-+        dsrc.get_data()
-+        self.assertIsNotNone(dsrc.metadata)
-+        self.assertTrue(dsrc.failed_desired_api_version)
-+
-+    @mock.patch(
-+        MOCKPATH + 'get_metadata_from_imds', return_value=NETWORK_METADATA)
-+    def test_imds_api_version_wanted_exists(self, m_get_metadata_from_imds):
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        dsrc = self._get_ds(data)
-+        dsrc.get_data()
-+        self.assertIsNotNone(dsrc.metadata)
-+        self.assertFalse(dsrc.failed_desired_api_version)
-+
- 
- class TestAzureBounce(CiTestCase):
- 
-@@ -2657,7 +2693,7 @@ class TestPreprovisioningHotAttachNics(CiTestCase):
-     @mock.patch(MOCKPATH + 'DataSourceAzure.wait_for_link_up')
-     @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_attach_event')
-     @mock.patch('cloudinit.sources.net.find_fallback_nic')
--    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback')
-     @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
-     @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
-     @mock.patch('os.path.isfile')
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch b/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch
deleted file mode 100644
index de27366..0000000
--- a/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch
+++ /dev/null
@@ -1,397 +0,0 @@
-From 3ec4ddbc595c5fe781b3dc501631d23569849818 Mon Sep 17 00:00:00 2001
-From: Thomas Stringer <thstring@microsoft.com>
-Date: Mon, 26 Apr 2021 09:41:38 -0400
-Subject: [PATCH 5/7] Azure: Retrieve username and hostname from IMDS (#865)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [5/7] 6fab7ef28c7fd340bda4f82dbf828f10716cb3f1
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-This change allows us to retrieve the username and hostname from
-IMDS instead of having to rely on the mounted OVF.
----
- cloudinit/sources/DataSourceAzure.py          | 149 ++++++++++++++----
- tests/unittests/test_datasource/test_azure.py |  87 +++++++++-
- 2 files changed, 205 insertions(+), 31 deletions(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index 39e67c4f..6d7954ee 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -5,6 +5,7 @@
- # This file is part of cloud-init. See LICENSE file for license information.
- 
- import base64
-+from collections import namedtuple
- import contextlib
- import crypt
- from functools import partial
-@@ -25,6 +26,7 @@ from cloudinit.net import device_driver
- from cloudinit.net.dhcp import EphemeralDHCPv4
- from cloudinit import sources
- from cloudinit.sources.helpers import netlink
-+from cloudinit import ssh_util
- from cloudinit import subp
- from cloudinit.url_helper import UrlError, readurl, retry_on_url_exc
- from cloudinit import util
-@@ -80,7 +82,12 @@ AGENT_SEED_DIR = '/var/lib/waagent'
- IMDS_TIMEOUT_IN_SECONDS = 2
- IMDS_URL = "http://169.254.169.254/metadata"
- IMDS_VER_MIN = "2019-06-01"
--IMDS_VER_WANT = "2020-09-01"
-+IMDS_VER_WANT = "2020-10-01"
-+
-+
-+# This holds SSH key data including if the source was
-+# from IMDS, as well as the SSH key data itself.
-+SSHKeys = namedtuple("SSHKeys", ("keys_from_imds", "ssh_keys"))
- 
- 
- class metadata_type(Enum):
-@@ -391,6 +398,8 @@ class DataSourceAzure(sources.DataSource):
-         """Return the subplatform metadata source details."""
-         if self.seed.startswith('/dev'):
-             subplatform_type = 'config-disk'
-+        elif self.seed.lower() == 'imds':
-+            subplatform_type = 'imds'
-         else:
-             subplatform_type = 'seed-dir'
-         return '%s (%s)' % (subplatform_type, self.seed)
-@@ -433,9 +442,11 @@ class DataSourceAzure(sources.DataSource):
- 
-         found = None
-         reprovision = False
-+        ovf_is_accessible = True
-         reprovision_after_nic_attach = False
-         for cdev in candidates:
-             try:
-+                LOG.debug("cdev: %s", cdev)
-                 if cdev == "IMDS":
-                     ret = None
-                     reprovision = True
-@@ -462,8 +473,18 @@ class DataSourceAzure(sources.DataSource):
-                 raise sources.InvalidMetaDataException(msg)
-             except util.MountFailedError:
-                 report_diagnostic_event(
--                    '%s was not mountable' % cdev, logger_func=LOG.warning)
--                continue
-+                    '%s was not mountable' % cdev, logger_func=LOG.debug)
-+                cdev = 'IMDS'
-+                ovf_is_accessible = False
-+                empty_md = {'local-hostname': ''}
-+                empty_cfg = dict(
-+                    system_info=dict(
-+                        default_user=dict(
-+                            name=''
-+                        )
-+                    )
-+                )
-+                ret = (empty_md, '', empty_cfg, {})
- 
-             report_diagnostic_event("Found provisioning metadata in %s" % cdev,
-                                     logger_func=LOG.debug)
-@@ -490,6 +511,10 @@ class DataSourceAzure(sources.DataSource):
-                 self.fallback_interface,
-                 retries=10
-             )
-+            if not imds_md and not ovf_is_accessible:
-+                msg = 'No OVF or IMDS available'
-+                report_diagnostic_event(msg)
-+                raise sources.InvalidMetaDataException(msg)
-             (md, userdata_raw, cfg, files) = ret
-             self.seed = cdev
-             crawled_data.update({
-@@ -498,6 +523,21 @@ class DataSourceAzure(sources.DataSource):
-                 'metadata': util.mergemanydict(
-                     [md, {'imds': imds_md}]),
-                 'userdata_raw': userdata_raw})
-+            imds_username = _username_from_imds(imds_md)
-+            imds_hostname = _hostname_from_imds(imds_md)
-+            imds_disable_password = _disable_password_from_imds(imds_md)
-+            if imds_username:
-+                LOG.debug('Username retrieved from IMDS: %s', imds_username)
-+                cfg['system_info']['default_user']['name'] = imds_username
-+            if imds_hostname:
-+                LOG.debug('Hostname retrieved from IMDS: %s', imds_hostname)
-+                crawled_data['metadata']['local-hostname'] = imds_hostname
-+            if imds_disable_password:
-+                LOG.debug(
-+                    'Disable password retrieved from IMDS: %s',
-+                    imds_disable_password
-+                )
-+                crawled_data['metadata']['disable_password'] = imds_disable_password  # noqa: E501
-             found = cdev
- 
-             report_diagnostic_event(
-@@ -676,6 +716,13 @@ class DataSourceAzure(sources.DataSource):
- 
-     @azure_ds_telemetry_reporter
-     def get_public_ssh_keys(self):
-+        """
-+        Retrieve public SSH keys.
-+        """
-+
-+        return self._get_public_ssh_keys_and_source().ssh_keys
-+
-+    def _get_public_ssh_keys_and_source(self):
-         """
-         Try to get the ssh keys from IMDS first, and if that fails
-         (i.e. IMDS is unavailable) then fallback to getting the ssh
-@@ -685,30 +732,50 @@ class DataSourceAzure(sources.DataSource):
-         advantage, so this is a strong preference. But we must keep
-         OVF as a second option for environments that don't have IMDS.
-         """
-+
-         LOG.debug('Retrieving public SSH keys')
-         ssh_keys = []
-+        keys_from_imds = True
-+        LOG.debug('Attempting to get SSH keys from IMDS')
-         try:
--            raise KeyError(
--                "Not using public SSH keys from IMDS"
--            )
--            # pylint:disable=unreachable
-             ssh_keys = [
-                 public_key['keyData']
-                 for public_key
-                 in self.metadata['imds']['compute']['publicKeys']
-             ]
--            LOG.debug('Retrieved SSH keys from IMDS')
-+            for key in ssh_keys:
-+                if not _key_is_openssh_formatted(key=key):
-+                    keys_from_imds = False
-+                    break
-+
-+            if not keys_from_imds:
-+                log_msg = 'Keys not in OpenSSH format, using OVF'
-+            else:
-+                log_msg = 'Retrieved {} keys from IMDS'.format(
-+                    len(ssh_keys)
-+                    if ssh_keys is not None
-+                    else 0
-+                )
-         except KeyError:
-             log_msg = 'Unable to get keys from IMDS, falling back to OVF'
-+            keys_from_imds = False
-+        finally:
-             report_diagnostic_event(log_msg, logger_func=LOG.debug)
-+
-+        if not keys_from_imds:
-+            LOG.debug('Attempting to get SSH keys from OVF')
-             try:
-                 ssh_keys = self.metadata['public-keys']
--                LOG.debug('Retrieved keys from OVF')
-+                log_msg = 'Retrieved {} keys from OVF'.format(len(ssh_keys))
-             except KeyError:
-                 log_msg = 'No keys available from OVF'
-+            finally:
-                 report_diagnostic_event(log_msg, logger_func=LOG.debug)
- 
--        return ssh_keys
-+        return SSHKeys(
-+            keys_from_imds=keys_from_imds,
-+            ssh_keys=ssh_keys
-+        )
- 
-     def get_config_obj(self):
-         return self.cfg
-@@ -1325,30 +1392,21 @@ class DataSourceAzure(sources.DataSource):
-         self.bounce_network_with_azure_hostname()
- 
-         pubkey_info = None
--        try:
--            raise KeyError(
--                "Not using public SSH keys from IMDS"
--            )
--            # pylint:disable=unreachable
--            public_keys = self.metadata['imds']['compute']['publicKeys']
--            LOG.debug(
--                'Successfully retrieved %s key(s) from IMDS',
--                len(public_keys)
--                if public_keys is not None
-+        ssh_keys_and_source = self._get_public_ssh_keys_and_source()
-+
-+        if not ssh_keys_and_source.keys_from_imds:
-+            pubkey_info = self.cfg.get('_pubkeys', None)
-+            log_msg = 'Retrieved {} fingerprints from OVF'.format(
-+                len(pubkey_info)
-+                if pubkey_info is not None
-                 else 0
-             )
--        except KeyError:
--            LOG.debug(
--                'Unable to retrieve SSH keys from IMDS during '
--                'negotiation, falling back to OVF'
--            )
--            pubkey_info = self.cfg.get('_pubkeys', None)
-+            report_diagnostic_event(log_msg, logger_func=LOG.debug)
- 
-         metadata_func = partial(get_metadata_from_fabric,
-                                 fallback_lease_file=self.
-                                 dhclient_lease_file,
--                                pubkey_info=pubkey_info,
--                                iso_dev=self.iso_dev)
-+                                pubkey_info=pubkey_info)
- 
-         LOG.debug("negotiating with fabric via agent command %s",
-                   self.ds_cfg['agent_command'])
-@@ -1404,6 +1462,41 @@ class DataSourceAzure(sources.DataSource):
-         return self.metadata.get('imds', {}).get('compute', {}).get('location')
- 
- 
-+def _username_from_imds(imds_data):
-+    try:
-+        return imds_data['compute']['osProfile']['adminUsername']
-+    except KeyError:
-+        return None
-+
-+
-+def _hostname_from_imds(imds_data):
-+    try:
-+        return imds_data['compute']['osProfile']['computerName']
-+    except KeyError:
-+        return None
-+
-+
-+def _disable_password_from_imds(imds_data):
-+    try:
-+        return imds_data['compute']['osProfile']['disablePasswordAuthentication'] == 'true'  # noqa: E501
-+    except KeyError:
-+        return None
-+
-+
-+def _key_is_openssh_formatted(key):
-+    """
-+    Validate whether or not the key is OpenSSH-formatted.
-+    """
-+
-+    parser = ssh_util.AuthKeyLineParser()
-+    try:
-+        akl = parser.parse(key)
-+    except TypeError:
-+        return False
-+
-+    return akl.keytype is not None
-+
-+
- def _partitions_on_device(devpath, maxnum=16):
-     # return a list of tuples (ptnum, path) for each part on devpath
-     for suff in ("-part", "p", ""):
-diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
-index 320fa857..d9817d84 100644
---- a/tests/unittests/test_datasource/test_azure.py
-+++ b/tests/unittests/test_datasource/test_azure.py
-@@ -108,7 +108,7 @@ NETWORK_METADATA = {
-         "zone": "",
-         "publicKeys": [
-             {
--                "keyData": "key1",
-+                "keyData": "ssh-rsa key1",
-                 "path": "path1"
-             }
-         ]
-@@ -1761,8 +1761,29 @@ scbus-1 on xpt0 bus 0
-         dsrc.get_data()
-         dsrc.setup(True)
-         ssh_keys = dsrc.get_public_ssh_keys()
--        # Temporarily alter this test so that SSH public keys
--        # from IMDS are *not* going to be in use to fix a regression.
-+        self.assertEqual(ssh_keys, ["ssh-rsa key1"])
-+        self.assertEqual(m_parse_certificates.call_count, 0)
-+
-+    @mock.patch(
-+        'cloudinit.sources.helpers.azure.OpenSSLManager.parse_certificates')
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_get_public_ssh_keys_with_no_openssh_format(
-+            self,
-+            m_get_metadata_from_imds,
-+            m_parse_certificates):
-+        imds_data = copy.deepcopy(NETWORK_METADATA)
-+        imds_data['compute']['publicKeys'][0]['keyData'] = 'no-openssh-format'
-+        m_get_metadata_from_imds.return_value = imds_data
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        dsrc = self._get_ds(data)
-+        dsrc.get_data()
-+        dsrc.setup(True)
-+        ssh_keys = dsrc.get_public_ssh_keys()
-         self.assertEqual(ssh_keys, [])
-         self.assertEqual(m_parse_certificates.call_count, 0)
- 
-@@ -1818,6 +1839,66 @@ scbus-1 on xpt0 bus 0
-         self.assertIsNotNone(dsrc.metadata)
-         self.assertFalse(dsrc.failed_desired_api_version)
- 
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_hostname_from_imds(self, m_get_metadata_from_imds):
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA)
-+        imds_data_with_os_profile["compute"]["osProfile"] = dict(
-+            adminUsername="username1",
-+            computerName="hostname1",
-+            disablePasswordAuthentication="true"
-+        )
-+        m_get_metadata_from_imds.return_value = imds_data_with_os_profile
-+        dsrc = self._get_ds(data)
-+        dsrc.get_data()
-+        self.assertEqual(dsrc.metadata["local-hostname"], "hostname1")
-+
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_username_from_imds(self, m_get_metadata_from_imds):
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA)
-+        imds_data_with_os_profile["compute"]["osProfile"] = dict(
-+            adminUsername="username1",
-+            computerName="hostname1",
-+            disablePasswordAuthentication="true"
-+        )
-+        m_get_metadata_from_imds.return_value = imds_data_with_os_profile
-+        dsrc = self._get_ds(data)
-+        dsrc.get_data()
-+        self.assertEqual(
-+            dsrc.cfg["system_info"]["default_user"]["name"],
-+            "username1"
-+        )
-+
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_disable_password_from_imds(self, m_get_metadata_from_imds):
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA)
-+        imds_data_with_os_profile["compute"]["osProfile"] = dict(
-+            adminUsername="username1",
-+            computerName="hostname1",
-+            disablePasswordAuthentication="true"
-+        )
-+        m_get_metadata_from_imds.return_value = imds_data_with_os_profile
-+        dsrc = self._get_ds(data)
-+        dsrc.get_data()
-+        self.assertTrue(dsrc.metadata["disable_password"])
-+
- 
- class TestAzureBounce(CiTestCase):
- 
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch b/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch
deleted file mode 100644
index efc9fc2..0000000
--- a/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch
+++ /dev/null
@@ -1,315 +0,0 @@
-From ca5b83cee7b45bf56eec258db739cb5fe51b3231 Mon Sep 17 00:00:00 2001
-From: aswinrajamannar <39812128+aswinrajamannar@users.noreply.github.com>
-Date: Mon, 26 Apr 2021 07:28:39 -0700
-Subject: [PATCH 6/7] Azure: Retry net metadata during nic attach for
- non-timeout errs (#878)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [6/7] 4e6e44f017d5ffcb72ac8959a94f80c71fef9560
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-When network interfaces are hot-attached to the VM, attempting to get
-network metadata might return 410 (or 500, 503 etc) because the info
-is not yet available. In those cases, we retry getting the metadata
-before giving up. The only case where we can move on to wait for more
-nic attach events is if the call times out despite retries, which
-means the interface is not likely a primary interface, and we should
-try for more nic attach events.
----
- cloudinit/sources/DataSourceAzure.py          | 65 +++++++++++--
- tests/unittests/test_datasource/test_azure.py | 95 ++++++++++++++++---
- 2 files changed, 140 insertions(+), 20 deletions(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index 6d7954ee..d0be6d84 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -17,6 +17,7 @@ from time import sleep
- from xml.dom import minidom
- import xml.etree.ElementTree as ET
- from enum import Enum
-+import requests
- 
- from cloudinit import dmi
- from cloudinit import log as logging
-@@ -665,7 +666,9 @@ class DataSourceAzure(sources.DataSource):
-             self,
-             fallback_nic,
-             retries,
--            md_type=metadata_type.compute):
-+            md_type=metadata_type.compute,
-+            exc_cb=retry_on_url_exc,
-+            infinite=False):
-         """
-         Wrapper for get_metadata_from_imds so that we can have flexibility
-         in which IMDS api-version we use. If a particular instance of IMDS
-@@ -685,7 +688,8 @@ class DataSourceAzure(sources.DataSource):
-                         fallback_nic=fallback_nic,
-                         retries=0,
-                         md_type=md_type,
--                        api_version=IMDS_VER_WANT
-+                        api_version=IMDS_VER_WANT,
-+                        exc_cb=exc_cb
-                     )
-                 except UrlError as err:
-                     LOG.info(
-@@ -708,7 +712,9 @@ class DataSourceAzure(sources.DataSource):
-             fallback_nic=fallback_nic,
-             retries=retries,
-             md_type=md_type,
--            api_version=IMDS_VER_MIN
-+            api_version=IMDS_VER_MIN,
-+            exc_cb=exc_cb,
-+            infinite=infinite
-         )
- 
-     def device_name_to_device(self, name):
-@@ -938,6 +944,9 @@ class DataSourceAzure(sources.DataSource):
-         is_primary = False
-         expected_nic_count = -1
-         imds_md = None
-+        metadata_poll_count = 0
-+        metadata_logging_threshold = 1
-+        metadata_timeout_count = 0
- 
-         # For now, only a VM's primary NIC can contact IMDS and WireServer. If
-         # DHCP fails for a NIC, we have no mechanism to determine if the NIC is
-@@ -962,14 +971,48 @@ class DataSourceAzure(sources.DataSource):
-                                     % (ifname, e), logger_func=LOG.error)
-             raise
- 
-+        # Retry polling network metadata for a limited duration only when the
-+        # calls fail due to timeout. This is because the platform drops packets
-+        # going towards IMDS when it is not a primary nic. If the calls fail
-+        # due to other issues like 410, 503 etc, then it means we are primary
-+        # but IMDS service is unavailable at the moment. Retry indefinitely in
-+        # those cases since we cannot move on without the network metadata.
-+        def network_metadata_exc_cb(msg, exc):
-+            nonlocal metadata_timeout_count, metadata_poll_count
-+            nonlocal metadata_logging_threshold
-+
-+            metadata_poll_count = metadata_poll_count + 1
-+
-+            # Log when needed but back off exponentially to avoid exploding
-+            # the log file.
-+            if metadata_poll_count >= metadata_logging_threshold:
-+                metadata_logging_threshold *= 2
-+                report_diagnostic_event(
-+                    "Ran into exception when attempting to reach %s "
-+                    "after %d polls." % (msg, metadata_poll_count),
-+                    logger_func=LOG.error)
-+
-+                if isinstance(exc, UrlError):
-+                    report_diagnostic_event("poll IMDS with %s failed. "
-+                                            "Exception: %s and code: %s" %
-+                                            (msg, exc.cause, exc.code),
-+                                            logger_func=LOG.error)
-+
-+            if exc.cause and isinstance(exc.cause, requests.Timeout):
-+                metadata_timeout_count = metadata_timeout_count + 1
-+                return (metadata_timeout_count <= 10)
-+            return True
-+
-         # Primary nic detection will be optimized in the future. The fact that
-         # primary nic is being attached first helps here. Otherwise each nic
-         # could add several seconds of delay.
-         try:
-             imds_md = self.get_imds_data_with_api_fallback(
-                 ifname,
--                5,
--                metadata_type.network
-+                0,
-+                metadata_type.network,
-+                network_metadata_exc_cb,
-+                True
-             )
-         except Exception as e:
-             LOG.warning(
-@@ -2139,7 +2182,9 @@ def _generate_network_config_from_fallback_config() -> dict:
- def get_metadata_from_imds(fallback_nic,
-                            retries,
-                            md_type=metadata_type.compute,
--                           api_version=IMDS_VER_MIN):
-+                           api_version=IMDS_VER_MIN,
-+                           exc_cb=retry_on_url_exc,
-+                           infinite=False):
-     """Query Azure's instance metadata service, returning a dictionary.
- 
-     If network is not up, setup ephemeral dhcp on fallback_nic to talk to the
-@@ -2158,7 +2203,7 @@ def get_metadata_from_imds(fallback_nic,
-     kwargs = {'logfunc': LOG.debug,
-               'msg': 'Crawl of Azure Instance Metadata Service (IMDS)',
-               'func': _get_metadata_from_imds,
--              'args': (retries, md_type, api_version,)}
-+              'args': (retries, exc_cb, md_type, api_version, infinite)}
-     if net.is_up(fallback_nic):
-         return util.log_time(**kwargs)
-     else:
-@@ -2176,14 +2221,16 @@ def get_metadata_from_imds(fallback_nic,
- @azure_ds_telemetry_reporter
- def _get_metadata_from_imds(
-         retries,
-+        exc_cb,
-         md_type=metadata_type.compute,
--        api_version=IMDS_VER_MIN):
-+        api_version=IMDS_VER_MIN,
-+        infinite=False):
-     url = "{}?api-version={}".format(md_type.value, api_version)
-     headers = {"Metadata": "true"}
-     try:
-         response = readurl(
-             url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers,
--            retries=retries, exception_cb=retry_on_url_exc)
-+            retries=retries, exception_cb=exc_cb, infinite=infinite)
-     except Exception as e:
-         # pylint:disable=no-member
-         if isinstance(e, UrlError) and e.code == 400:
-diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
-index d9817d84..c4a8e08d 100644
---- a/tests/unittests/test_datasource/test_azure.py
-+++ b/tests/unittests/test_datasource/test_azure.py
-@@ -448,7 +448,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
-             "http://169.254.169.254/metadata/instance?api-version="
-             "2019-06-01", exception_cb=mock.ANY,
-             headers=mock.ANY, retries=mock.ANY,
--            timeout=mock.ANY)
-+            timeout=mock.ANY, infinite=False)
- 
-     @mock.patch(MOCKPATH + 'readurl', autospec=True)
-     @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
-@@ -467,7 +467,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
-             "http://169.254.169.254/metadata/instance/network?api-version="
-             "2019-06-01", exception_cb=mock.ANY,
-             headers=mock.ANY, retries=mock.ANY,
--            timeout=mock.ANY)
-+            timeout=mock.ANY, infinite=False)
- 
-     @mock.patch(MOCKPATH + 'readurl', autospec=True)
-     @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
-@@ -486,7 +486,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
-             "http://169.254.169.254/metadata/instance?api-version="
-             "2019-06-01", exception_cb=mock.ANY,
-             headers=mock.ANY, retries=mock.ANY,
--            timeout=mock.ANY)
-+            timeout=mock.ANY, infinite=False)
- 
-     @mock.patch(MOCKPATH + 'readurl', autospec=True)
-     @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting', autospec=True)
-@@ -511,7 +511,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
-         m_readurl.assert_called_with(
-             self.network_md_url, exception_cb=mock.ANY,
-             headers={'Metadata': 'true'}, retries=2,
--            timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS)
-+            timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS, infinite=False)
- 
-     @mock.patch('cloudinit.url_helper.time.sleep')
-     @mock.patch(MOCKPATH + 'net.is_up', autospec=True)
-@@ -2694,15 +2694,22 @@ class TestPreprovisioningHotAttachNics(CiTestCase):
- 
-         def nic_attach_ret(nl_sock, nics_found):
-             nonlocal m_attach_call_count
--            if m_attach_call_count == 0:
--                m_attach_call_count = m_attach_call_count + 1
-+            m_attach_call_count = m_attach_call_count + 1
-+            if m_attach_call_count == 1:
-                 return "eth0"
--            return "eth1"
-+            elif m_attach_call_count == 2:
-+                return "eth1"
-+            raise RuntimeError("Must have found primary nic by now.")
-+
-+        # Simulate two NICs by adding the same one twice.
-+        md = {
-+            "interface": [
-+                IMDS_NETWORK_METADATA['interface'][0],
-+                IMDS_NETWORK_METADATA['interface'][0]
-+            ]
-+        }
- 
--        def network_metadata_ret(ifname, retries, type):
--            # Simulate two NICs by adding the same one twice.
--            md = IMDS_NETWORK_METADATA
--            md['interface'].append(md['interface'][0])
-+        def network_metadata_ret(ifname, retries, type, exc_cb, infinite):
-             if ifname == "eth0":
-                 return md
-             raise requests.Timeout('Fake connection timeout')
-@@ -2724,6 +2731,72 @@ class TestPreprovisioningHotAttachNics(CiTestCase):
-         self.assertEqual(1, m_imds.call_count)
-         self.assertEqual(2, m_link_up.call_count)
- 
-+    @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback')
-+    @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
-+    def test_check_if_nic_is_primary_retries_on_failures(
-+            self, m_dhcpv4, m_imds):
-+        """Retry polling for network metadata on all failures except timeout"""
-+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
-+        lease = {
-+            'interface': 'eth9', 'fixed-address': '192.168.2.9',
-+            'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
-+            'unknown-245': '624c3620'}
-+
-+        eth0Retries = []
-+        eth1Retries = []
-+        # Simulate two NICs by adding the same one twice.
-+        md = {
-+            "interface": [
-+                IMDS_NETWORK_METADATA['interface'][0],
-+                IMDS_NETWORK_METADATA['interface'][0]
-+            ]
-+        }
-+
-+        def network_metadata_ret(ifname, retries, type, exc_cb, infinite):
-+            nonlocal eth0Retries, eth1Retries
-+
-+            # Simulate readurl functionality with retries and
-+            # exception callbacks so that the callback logic can be
-+            # validated.
-+            if ifname == "eth0":
-+                cause = requests.HTTPError()
-+                for _ in range(0, 15):
-+                    error = url_helper.UrlError(cause=cause, code=410)
-+                    eth0Retries.append(exc_cb("No goal state.", error))
-+            else:
-+                cause = requests.Timeout('Fake connection timeout')
-+                for _ in range(0, 10):
-+                    error = url_helper.UrlError(cause=cause)
-+                    eth1Retries.append(exc_cb("Connection timeout", error))
-+                # Should stop retrying after 10 retries
-+                eth1Retries.append(exc_cb("Connection timeout", error))
-+                raise cause
-+            return md
-+
-+        m_imds.side_effect = network_metadata_ret
-+
-+        dhcp_ctx = mock.MagicMock(lease=lease)
-+        dhcp_ctx.obtain_lease.return_value = lease
-+        m_dhcpv4.return_value = dhcp_ctx
-+
-+        is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth0")
-+        self.assertEqual(True, is_primary)
-+        self.assertEqual(2, expected_nic_count)
-+
-+        # All Eth0 errors are non-timeout errors. So we should have been
-+        # retrying indefinitely until success.
-+        for i in eth0Retries:
-+            self.assertTrue(i)
-+
-+        is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth1")
-+        self.assertEqual(False, is_primary)
-+
-+        # All Eth1 errors are timeout errors. Retry happens for a max of 10 and
-+        # then we should have moved on assuming it is not the primary nic.
-+        for i in range(0, 10):
-+            self.assertTrue(eth1Retries[i])
-+        self.assertFalse(eth1Retries[10])
-+
-     @mock.patch('cloudinit.distros.networking.LinuxNetworking.try_set_link_up')
-     def test_wait_for_link_up_returns_if_already_up(
-             self, m_is_link_up):
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch b/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch
deleted file mode 100644
index d4e7e37..0000000
--- a/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch
+++ /dev/null
@@ -1,129 +0,0 @@
-From c0df7233fa99d4191b5d4142e209e7465d8db5f6 Mon Sep 17 00:00:00 2001
-From: Anh Vo <anhvo@microsoft.com>
-Date: Tue, 27 Apr 2021 13:40:59 -0400
-Subject: [PATCH 7/7] Azure: adding support for consuming userdata from IMDS
- (#884)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [7/7] 32f840412da1a0f49b9ab5ba1d6f1bcb1bfacc16
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
----
- cloudinit/sources/DataSourceAzure.py          | 23 ++++++++-
- tests/unittests/test_datasource/test_azure.py | 50 +++++++++++++++++++
- 2 files changed, 72 insertions(+), 1 deletion(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index d0be6d84..a66f023d 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -83,7 +83,7 @@ AGENT_SEED_DIR = '/var/lib/waagent'
- IMDS_TIMEOUT_IN_SECONDS = 2
- IMDS_URL = "http://169.254.169.254/metadata"
- IMDS_VER_MIN = "2019-06-01"
--IMDS_VER_WANT = "2020-10-01"
-+IMDS_VER_WANT = "2021-01-01"
- 
- 
- # This holds SSH key data including if the source was
-@@ -539,6 +539,20 @@ class DataSourceAzure(sources.DataSource):
-                     imds_disable_password
-                 )
-                 crawled_data['metadata']['disable_password'] = imds_disable_password  # noqa: E501
-+
-+            # only use userdata from imds if OVF did not provide custom data
-+            # userdata provided by IMDS is always base64 encoded
-+            if not userdata_raw:
-+                imds_userdata = _userdata_from_imds(imds_md)
-+                if imds_userdata:
-+                    LOG.debug("Retrieved userdata from IMDS")
-+                    try:
-+                        crawled_data['userdata_raw'] = base64.b64decode(
-+                            ''.join(imds_userdata.split()))
-+                    except Exception:
-+                        report_diagnostic_event(
-+                            "Bad userdata in IMDS",
-+                            logger_func=LOG.warning)
-             found = cdev
- 
-             report_diagnostic_event(
-@@ -1512,6 +1526,13 @@ def _username_from_imds(imds_data):
-         return None
- 
- 
-+def _userdata_from_imds(imds_data):
-+    try:
-+        return imds_data['compute']['userData']
-+    except KeyError:
-+        return None
-+
-+
- def _hostname_from_imds(imds_data):
-     try:
-         return imds_data['compute']['osProfile']['computerName']
-diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
-index c4a8e08d..f8433690 100644
---- a/tests/unittests/test_datasource/test_azure.py
-+++ b/tests/unittests/test_datasource/test_azure.py
-@@ -1899,6 +1899,56 @@ scbus-1 on xpt0 bus 0
-         dsrc.get_data()
-         self.assertTrue(dsrc.metadata["disable_password"])
- 
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_userdata_from_imds(self, m_get_metadata_from_imds):
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        odata = {'HostName': "myhost", 'UserName': "myuser"}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+        userdata = "userdataImds"
-+        imds_data = copy.deepcopy(NETWORK_METADATA)
-+        imds_data["compute"]["osProfile"] = dict(
-+            adminUsername="username1",
-+            computerName="hostname1",
-+            disablePasswordAuthentication="true",
-+        )
-+        imds_data["compute"]["userData"] = b64e(userdata)
-+        m_get_metadata_from_imds.return_value = imds_data
-+        dsrc = self._get_ds(data)
-+        ret = dsrc.get_data()
-+        self.assertTrue(ret)
-+        self.assertEqual(dsrc.userdata_raw, userdata.encode('utf-8'))
-+
-+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
-+    def test_userdata_from_imds_with_customdata_from_OVF(
-+            self, m_get_metadata_from_imds):
-+        userdataOVF = "userdataOVF"
-+        odata = {
-+            'HostName': "myhost", 'UserName': "myuser",
-+            'UserData': {'text': b64e(userdataOVF), 'encoding': 'base64'}
-+        }
-+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
-+        data = {
-+            'ovfcontent': construct_valid_ovf_env(data=odata),
-+            'sys_cfg': sys_cfg
-+        }
-+
-+        userdataImds = "userdataImds"
-+        imds_data = copy.deepcopy(NETWORK_METADATA)
-+        imds_data["compute"]["osProfile"] = dict(
-+            adminUsername="username1",
-+            computerName="hostname1",
-+            disablePasswordAuthentication="true",
-+        )
-+        imds_data["compute"]["userData"] = b64e(userdataImds)
-+        m_get_metadata_from_imds.return_value = imds_data
-+        dsrc = self._get_ds(data)
-+        ret = dsrc.get_data()
-+        self.assertTrue(ret)
-+        self.assertEqual(dsrc.userdata_raw, userdataOVF.encode('utf-8'))
-+
- 
- class TestAzureBounce(CiTestCase):
- 
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch b/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch
deleted file mode 100644
index 6f6c109..0000000
--- a/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch
+++ /dev/null
@@ -1,177 +0,0 @@
-From 01489fb91f64f6137ddf88c39feabe4296f3a156 Mon Sep 17 00:00:00 2001
-From: Anh Vo <anhvo@microsoft.com>
-Date: Fri, 23 Apr 2021 10:18:05 -0400
-Subject: [PATCH 4/7] Azure: eject the provisioning iso before reporting ready
- (#861)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [4/7] ba830546a62ac5bea33b91d133d364a897b9f6c0
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-Due to hyper-v implementations, iso ejection is more efficient if performed
-from within the guest. The code will attempt to perform a best-effort ejection.
-Failure during ejection will not prevent reporting ready from happening. If iso
-ejection is successful, later iso ejection from the platform will be a no-op.
-In the event the iso ejection from the guest fails, iso ejection will still happen at
-the platform level.
----
- cloudinit/sources/DataSourceAzure.py          | 22 +++++++++++++++---
- cloudinit/sources/helpers/azure.py            | 23 ++++++++++++++++---
- .../test_datasource/test_azure_helper.py      | 13 +++++++++--
- 3 files changed, 50 insertions(+), 8 deletions(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index 020b7006..39e67c4f 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -332,6 +332,7 @@ class DataSourceAzure(sources.DataSource):
-     dsname = 'Azure'
-     _negotiated = False
-     _metadata_imds = sources.UNSET
-+    _ci_pkl_version = 1
- 
-     def __init__(self, sys_cfg, distro, paths):
-         sources.DataSource.__init__(self, sys_cfg, distro, paths)
-@@ -346,8 +347,13 @@ class DataSourceAzure(sources.DataSource):
-         # Regenerate network config new_instance boot and every boot
-         self.update_events['network'].add(EventType.BOOT)
-         self._ephemeral_dhcp_ctx = None
--
-         self.failed_desired_api_version = False
-+        self.iso_dev = None
-+
-+    def _unpickle(self, ci_pkl_version: int) -> None:
-+        super()._unpickle(ci_pkl_version)
-+        if "iso_dev" not in self.__dict__:
-+            self.iso_dev = None
- 
-     def __str__(self):
-         root = sources.DataSource.__str__(self)
-@@ -459,6 +465,13 @@ class DataSourceAzure(sources.DataSource):
-                     '%s was not mountable' % cdev, logger_func=LOG.warning)
-                 continue
- 
-+            report_diagnostic_event("Found provisioning metadata in %s" % cdev,
-+                                    logger_func=LOG.debug)
-+
-+            # save the iso device for ejection before reporting ready
-+            if cdev.startswith("/dev"):
-+                self.iso_dev = cdev
-+
-             perform_reprovision = reprovision or self._should_reprovision(ret)
-             perform_reprovision_after_nic_attach = (
-                 reprovision_after_nic_attach or
-@@ -1226,7 +1239,9 @@ class DataSourceAzure(sources.DataSource):
-         @return: The success status of sending the ready signal.
-         """
-         try:
--            get_metadata_from_fabric(None, lease['unknown-245'])
-+            get_metadata_from_fabric(fallback_lease_file=None,
-+                                     dhcp_opts=lease['unknown-245'],
-+                                     iso_dev=self.iso_dev)
-             return True
-         except Exception as e:
-             report_diagnostic_event(
-@@ -1332,7 +1347,8 @@ class DataSourceAzure(sources.DataSource):
-         metadata_func = partial(get_metadata_from_fabric,
-                                 fallback_lease_file=self.
-                                 dhclient_lease_file,
--                                pubkey_info=pubkey_info)
-+                                pubkey_info=pubkey_info,
-+                                iso_dev=self.iso_dev)
- 
-         LOG.debug("negotiating with fabric via agent command %s",
-                   self.ds_cfg['agent_command'])
-diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
-index 03e7156b..ad476076 100755
---- a/cloudinit/sources/helpers/azure.py
-+++ b/cloudinit/sources/helpers/azure.py
-@@ -865,7 +865,19 @@ class WALinuxAgentShim:
-         return endpoint_ip_address
- 
-     @azure_ds_telemetry_reporter
--    def register_with_azure_and_fetch_data(self, pubkey_info=None) -> dict:
-+    def eject_iso(self, iso_dev) -> None:
-+        try:
-+            LOG.debug("Ejecting the provisioning iso")
-+            subp.subp(['eject', iso_dev])
-+        except Exception as e:
-+            report_diagnostic_event(
-+                "Failed ejecting the provisioning iso: %s" % e,
-+                logger_func=LOG.debug)
-+
-+    @azure_ds_telemetry_reporter
-+    def register_with_azure_and_fetch_data(self,
-+                                           pubkey_info=None,
-+                                           iso_dev=None) -> dict:
-         """Gets the VM's GoalState from Azure, uses the GoalState information
-         to report ready/send the ready signal/provisioning complete signal to
-         Azure, and then uses pubkey_info to filter and obtain the user's
-@@ -891,6 +903,10 @@ class WALinuxAgentShim:
-             ssh_keys = self._get_user_pubkeys(goal_state, pubkey_info)
-         health_reporter = GoalStateHealthReporter(
-             goal_state, self.azure_endpoint_client, self.endpoint)
-+
-+        if iso_dev is not None:
-+            self.eject_iso(iso_dev)
-+
-         health_reporter.send_ready_signal()
-         return {'public-keys': ssh_keys}
- 
-@@ -1046,11 +1062,12 @@ class WALinuxAgentShim:
- 
- @azure_ds_telemetry_reporter
- def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None,
--                             pubkey_info=None):
-+                             pubkey_info=None, iso_dev=None):
-     shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,
-                             dhcp_options=dhcp_opts)
-     try:
--        return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info)
-+        return shim.register_with_azure_and_fetch_data(
-+            pubkey_info=pubkey_info, iso_dev=iso_dev)
-     finally:
-         shim.clean_up()
- 
-diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
-index 63482c6c..552c7905 100644
---- a/tests/unittests/test_datasource/test_azure_helper.py
-+++ b/tests/unittests/test_datasource/test_azure_helper.py
-@@ -1009,6 +1009,14 @@ class TestWALinuxAgentShim(CiTestCase):
-         self.GoalState.return_value.container_id = self.test_container_id
-         self.GoalState.return_value.instance_id = self.test_instance_id
- 
-+    def test_eject_iso_is_called(self):
-+        shim = wa_shim()
-+        with mock.patch.object(
-+            shim, 'eject_iso', autospec=True
-+        ) as m_eject_iso:
-+            shim.register_with_azure_and_fetch_data(iso_dev="/dev/sr0")
-+            m_eject_iso.assert_called_once_with("/dev/sr0")
-+
-     def test_http_client_does_not_use_certificate_for_report_ready(self):
-         shim = wa_shim()
-         shim.register_with_azure_and_fetch_data()
-@@ -1283,13 +1291,14 @@ class TestGetMetadataGoalStateXMLAndReportReadyToFabric(CiTestCase):
- 
-     def test_calls_shim_register_with_azure_and_fetch_data(self):
-         m_pubkey_info = mock.MagicMock()
--        azure_helper.get_metadata_from_fabric(pubkey_info=m_pubkey_info)
-+        azure_helper.get_metadata_from_fabric(
-+            pubkey_info=m_pubkey_info, iso_dev="/dev/sr0")
-         self.assertEqual(
-             1,
-             self.m_shim.return_value
-                 .register_with_azure_and_fetch_data.call_count)
-         self.assertEqual(
--            mock.call(pubkey_info=m_pubkey_info),
-+            mock.call(iso_dev="/dev/sr0", pubkey_info=m_pubkey_info),
-             self.m_shim.return_value
-                 .register_with_azure_and_fetch_data.call_args)
- 
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch b/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch
deleted file mode 100644
index 627fd2b..0000000
--- a/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch
+++ /dev/null
@@ -1,90 +0,0 @@
-From f11bbe7f04a48eebcb446e283820d7592f76cf86 Mon Sep 17 00:00:00 2001
-From: Johnson Shi <Johnson.Shi@microsoft.com>
-Date: Thu, 25 Mar 2021 07:20:10 -0700
-Subject: [PATCH 2/7] Azure helper: Ensure Azure http handler sleeps between
- retries (#842)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [2/7] e8f8bb658b629a8444bd2ba19f109952acf33311
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-Ensure that the Azure helper's http handler sleeps a fixed duration
-between retry failure attempts. The http handler will sleep a fixed
-duration between failed attempts regardless of whether the attempt
-failed due to (1) request timing out or (2) instant failure (no
-timeout).
-
-Due to certain platform issues, the http request to the Azure endpoint
-may instantly fail without reaching the http timeout duration. Without
-sleeping a fixed duration in between retry attempts, the http handler
-will loop through the max retry attempts quickly. This causes the
-communication between cloud-init and the Azure platform to be less
-resilient due to the short total duration if there is no sleep in
-between retries.
----
- cloudinit/sources/helpers/azure.py                   |  2 ++
- tests/unittests/test_datasource/test_azure_helper.py | 11 +++++++++--
- 2 files changed, 11 insertions(+), 2 deletions(-)
-
-diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
-index d3055d08..03e7156b 100755
---- a/cloudinit/sources/helpers/azure.py
-+++ b/cloudinit/sources/helpers/azure.py
-@@ -303,6 +303,7 @@ def http_with_retries(url, **kwargs) -> str:
- 
-     max_readurl_attempts = 240
-     default_readurl_timeout = 5
-+    sleep_duration_between_retries = 5
-     periodic_logging_attempts = 12
- 
-     if 'timeout' not in kwargs:
-@@ -338,6 +339,7 @@ def http_with_retries(url, **kwargs) -> str:
-                     'attempt %d with exception: %s' %
-                     (url, attempt, e),
-                     logger_func=LOG.debug)
-+            time.sleep(sleep_duration_between_retries)
- 
-     raise exc
- 
-diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
-index b8899807..63482c6c 100644
---- a/tests/unittests/test_datasource/test_azure_helper.py
-+++ b/tests/unittests/test_datasource/test_azure_helper.py
-@@ -384,6 +384,7 @@ class TestAzureHelperHttpWithRetries(CiTestCase):
- 
-     max_readurl_attempts = 240
-     default_readurl_timeout = 5
-+    sleep_duration_between_retries = 5
-     periodic_logging_attempts = 12
- 
-     def setUp(self):
-@@ -394,8 +395,8 @@ class TestAzureHelperHttpWithRetries(CiTestCase):
-         self.m_readurl = patches.enter_context(
-             mock.patch.object(
-                 azure_helper.url_helper, 'readurl', mock.MagicMock()))
--        patches.enter_context(
--            mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock()))
-+        self.m_sleep = patches.enter_context(
-+            mock.patch.object(azure_helper.time, 'sleep', autospec=True))
- 
-     def test_http_with_retries(self):
-         self.m_readurl.return_value = 'TestResp'
-@@ -438,6 +439,12 @@ class TestAzureHelperHttpWithRetries(CiTestCase):
-             self.m_readurl.call_count,
-             self.periodic_logging_attempts + 1)
- 
-+        # Ensure that cloud-init did sleep between each failed request
-+        self.assertEqual(
-+            self.m_sleep.call_count,
-+            self.periodic_logging_attempts)
-+        self.m_sleep.assert_called_with(self.sleep_duration_between_retries)
-+
-     def test_http_with_retries_long_delay_logs_periodic_failure_msg(self):
-         self.m_readurl.side_effect = \
-             [SentinelException] * self.periodic_logging_attempts + \
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch b/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch
deleted file mode 100644
index 32fe4ac..0000000
--- a/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch
+++ /dev/null
@@ -1,47 +0,0 @@
-From c3d41dc6b18df0d74f569b1a0ba43c8118437948 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 14 Jan 2022 16:40:24 +0100
-Subject: [PATCH 3/6] Change netifaces dependency to 0.10.4 (#965)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 44: Datasource for VMware
-RH-Commit: [3/6] d25d68427ab8b86ee1521c66483e9300e8fcc735
-RH-Bugzilla: 2026587
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-commit b9d308b4d61d22bacc05bcae59819755975631f8
-Author: Andrew Kutz <101085+akutz@users.noreply.github.com>
-Date:   Tue Aug 10 15:10:44 2021 -0500
-
-    Change netifaces dependency to 0.10.4 (#965)
-
-    Change netifaces dependency to 0.10.4
-
-    Currently versions Ubuntu <=20.10 use netifaces 0.10.4 By requiring
-    netifaces 0.10.9, the VMware datasource omitted itself from cloud-init
-    on Ubuntu <=20.10.
-
-    This patch changes the netifaces dependency to 0.10.4. While it is true
-    there are patches to netifaces post 0.10.4 that are desirable, testing
-    against the most common network configuration was performed to verify
-    the VMware datasource will still function with netifaces 0.10.4.
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- requirements.txt | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/requirements.txt b/requirements.txt
-index 41d01d62..c4adc455 100644
---- a/requirements.txt
-+++ b/requirements.txt
-@@ -40,4 +40,4 @@ jsonschema
- # and still participate in instance-data by gathering the network in detail at
- # runtime and merge that information into the metadata and repersist that to
- # disk.
--netifaces>=0.10.9
-+netifaces>=0.10.4
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Datasource-for-VMware-953.patch b/SOURCES/ci-Datasource-for-VMware-953.patch
deleted file mode 100644
index 137ee07..0000000
--- a/SOURCES/ci-Datasource-for-VMware-953.patch
+++ /dev/null
@@ -1,2198 +0,0 @@
-From 1917af220242840ec1b21f82f80532cf6548cc00 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 14 Jan 2022 16:34:49 +0100
-Subject: [PATCH 2/6] Datasource for VMware (#953)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 44: Datasource for VMware
-RH-Commit: [2/6] bb6e58dfeaf8b64d2801ddb4cb73868cf31de3ef
-RH-Bugzilla: 2026587
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-commit 8b4a9bc7b81e61943af873bad92e2133f8275b0b
-Author: Andrew Kutz <101085+akutz@users.noreply.github.com>
-Date:   Mon Aug 9 21:24:07 2021 -0500
-
-    Datasource for VMware (#953)
-
-    This patch finally introduces the Cloud-Init Datasource for VMware
-    GuestInfo as a part of cloud-init proper. This datasource has existed
-    since 2018, and rapidly became the de facto datasource for developers
-    working with Packer, Terraform, for projects like kube-image-builder,
-    and the de jure datasource for Photon OS.
-
-    The major change to the datasource from its previous incarnation is
-    the name. Now named DatasourceVMware, this new version of the
-    datasource will allow multiple transport types in addition to
-    GuestInfo keys.
-
-    This datasource includes several unique features developed to address
-    real-world situations:
-
-      * Support for reading any key (metadata, userdata, vendordata) both
-        from the guestinfo table when running on a VM in vSphere as well as
-        from an environment variable when running inside of a container,
-        useful for rapid dev/test.
-
-      * Allows booting with DHCP while still providing full participation
-        in Cloud-Init instance data and Jinja queries. The netifaces library
-        provides the ability to inspect the network after it is online,
-        and the runtime network configuration is then merged into the
-        existing metadata and persisted to disk.
-
-      * Advertises the local_ipv4 and local_ipv6 addresses via guestinfo
-        as well. This is useful as Guest Tools is not always able to
-        identify what would be considered the local address.
-
-    The primary author and current steward of this datasource spoke at
-    Cloud-Init Con 2020 where there was interest in contributing this datasource
-    to the Cloud-Init codebase.
-
-    The datasource currently lives in its own GitHub repository at
-    https://github.com/vmware/cloud-init-vmware-guestinfo. Once the datasource
-    is merged into Cloud-Init, the old repository will be deprecated.
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- README.md                                     |   2 +-
- cloudinit/settings.py                         |   1 +
- cloudinit/sources/DataSourceVMware.py         | 871 ++++++++++++++++++
- doc/rtd/topics/availability.rst               |   1 +
- doc/rtd/topics/datasources.rst                |   2 +-
- doc/rtd/topics/datasources/vmware.rst         | 359 ++++++++
- requirements.txt                              |   9 +
- .../unittests/test_datasource/test_common.py  |   3 +
- .../unittests/test_datasource/test_vmware.py  | 377 ++++++++
- tests/unittests/test_ds_identify.py           | 279 +++++-
- tools/.github-cla-signers                     |   1 +
- tools/ds-identify                             |  76 +-
- 12 files changed, 1977 insertions(+), 4 deletions(-)
- create mode 100644 cloudinit/sources/DataSourceVMware.py
- create mode 100644 doc/rtd/topics/datasources/vmware.rst
- create mode 100644 tests/unittests/test_datasource/test_vmware.py
-
-diff --git a/README.md b/README.md
-index 435405da..aa4fad63 100644
---- a/README.md
-+++ b/README.md
-@@ -39,7 +39,7 @@ get in contact with that distribution and send them our way!
- 
- | Supported OSes | Supported Public Clouds | Supported Private Clouds |
- | --- | --- | --- |
--| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />|
-+| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br />VMware<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />|
- 
- ## To start developing cloud-init
- 
-diff --git a/cloudinit/settings.py b/cloudinit/settings.py
-index 2acf2615..d5f32dbb 100644
---- a/cloudinit/settings.py
-+++ b/cloudinit/settings.py
-@@ -42,6 +42,7 @@ CFG_BUILTIN = {
-         'Exoscale',
-         'RbxCloud',
-         'UpCloud',
-+        'VMware',
-         # At the end to act as a 'catch' when none of the above work...
-         'None',
-     ],
-diff --git a/cloudinit/sources/DataSourceVMware.py b/cloudinit/sources/DataSourceVMware.py
-new file mode 100644
-index 00000000..22ca63de
---- /dev/null
-+++ b/cloudinit/sources/DataSourceVMware.py
-@@ -0,0 +1,871 @@
-+# Cloud-Init DataSource for VMware
-+#
-+# Copyright (c) 2018-2021 VMware, Inc. All Rights Reserved.
-+#
-+# Authors: Anish Swaminathan <anishs@vmware.com>
-+#          Andrew Kutz <akutz@vmware.com>
-+#
-+# This file is part of cloud-init. See LICENSE file for license information.
-+
-+"""Cloud-Init DataSource for VMware
-+
-+This module provides a cloud-init datasource for VMware systems and supports
-+multiple transports types, including:
-+
-+    * EnvVars
-+    * GuestInfo
-+
-+Netifaces (https://github.com/al45tair/netifaces)
-+
-+    Please note this module relies on the netifaces project to introspect the
-+    runtime, network configuration of the host on which this datasource is
-+    running. This is in contrast to the rest of cloud-init which uses the
-+    cloudinit/netinfo module.
-+
-+    The reasons for using netifaces include:
-+
-+        * Netifaces is built in C and is more portable across multiple systems
-+          and more deterministic than shell exec'ing local network commands and
-+          parsing their output.
-+
-+        * Netifaces provides a stable way to determine the view of the host's
-+          network after DHCP has brought the network online. Unlike most other
-+          datasources, this datasource still provides support for JINJA queries
-+          based on networking information even when the network is based on a
-+          DHCP lease. While this does not tie this datasource directly to
-+          netifaces, it does mean the ability to consistently obtain the
-+          correct information is paramount.
-+
-+        * It is currently possible to execute this datasource on macOS
-+          (which many developers use today) to print the output of the
-+          get_host_info function. This function calls netifaces to obtain
-+          the same runtime network configuration that the datasource would
-+          persist to the local system's instance data.
-+
-+          However, the netinfo module fails on macOS. The result is either a
-+          hung operation that requires a SIGINT to return control to the user,
-+          or, if brew is used to install iproute2mac, the ip commands are used
-+          but produce output the netinfo module is unable to parse.
-+
-+          While macOS is not a target of cloud-init, this feature is quite
-+          useful when working on this datasource.
-+
-+          For more information about this behavior, please see the following
-+          PR comment, https://bit.ly/3fG7OVh.
-+
-+    The authors of this datasource are not opposed to moving away from
-+    netifaces. The goal may be to eventually do just that. This proviso was
-+    added to the top of this module as a way to remind future-us and others
-+    why netifaces was used in the first place in order to either smooth the
-+    transition away from netifaces or embrace it further up the cloud-init
-+    stack.
-+"""
-+
-+import collections
-+import copy
-+from distutils.spawn import find_executable
-+import ipaddress
-+import json
-+import os
-+import socket
-+import time
-+
-+from cloudinit import dmi, log as logging
-+from cloudinit import sources
-+from cloudinit import util
-+from cloudinit.subp import subp, ProcessExecutionError
-+
-+import netifaces
-+
-+
-+PRODUCT_UUID_FILE_PATH = "/sys/class/dmi/id/product_uuid"
-+
-+LOG = logging.getLogger(__name__)
-+NOVAL = "No value found"
-+
-+DATA_ACCESS_METHOD_ENVVAR = "envvar"
-+DATA_ACCESS_METHOD_GUESTINFO = "guestinfo"
-+
-+VMWARE_RPCTOOL = find_executable("vmware-rpctool")
-+REDACT = "redact"
-+CLEANUP_GUESTINFO = "cleanup-guestinfo"
-+VMX_GUESTINFO = "VMX_GUESTINFO"
-+GUESTINFO_EMPTY_YAML_VAL = "---"
-+
-+LOCAL_IPV4 = "local-ipv4"
-+LOCAL_IPV6 = "local-ipv6"
-+WAIT_ON_NETWORK = "wait-on-network"
-+WAIT_ON_NETWORK_IPV4 = "ipv4"
-+WAIT_ON_NETWORK_IPV6 = "ipv6"
-+
-+
-+class DataSourceVMware(sources.DataSource):
-+    """
-+    Setting the hostname:
-+        The hostname is set by way of the metadata key "local-hostname".
-+
-+    Setting the instance ID:
-+        The instance ID may be set by way of the metadata key "instance-id".
-+        However, if this value is absent then the instance ID is read
-+        from the file /sys/class/dmi/id/product_uuid.
-+
-+    Configuring the network:
-+        The network is configured by setting the metadata key "network"
-+        with a value consistent with Network Config Versions 1 or 2,
-+        depending on the Linux distro's version of cloud-init:
-+
-+            Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
-+            Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
-+
-+        For example, CentOS 7's official cloud-init package is version
-+        0.7.9 and does not support Network Config Version 2. However,
-+        this datasource still supports supplying Network Config Version 2
-+        data as long as the Linux distro's cloud-init package is new
-+        enough to parse the data.
-+
-+        The metadata key "network.encoding" may be used to indicate the
-+        format of the metadata key "network". Valid encodings are base64
-+        and gzip+base64.
-+    """
-+
-+    dsname = "VMware"
-+
-+    def __init__(self, sys_cfg, distro, paths, ud_proc=None):
-+        sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
-+
-+        self.data_access_method = None
-+        self.vmware_rpctool = VMWARE_RPCTOOL
-+
-+    def _get_data(self):
-+        """
-+        _get_data loads the metadata, userdata, and vendordata from one of
-+        the following locations in the given order:
-+
-+            * envvars
-+            * guestinfo
-+
-+        Please note when updating this function with support for new data
-+        transports, the order should match the order in the dscheck_VMware
-+        function from the file ds-identify.
-+        """
-+
-+        # Initialize the locally scoped metadata, userdata, and vendordata
-+        # variables. They are assigned below depending on the detected data
-+        # access method.
-+        md, ud, vd = None, None, None
-+
-+        # First check to see if there is data via env vars.
-+        if os.environ.get(VMX_GUESTINFO, ""):
-+            md = guestinfo_envvar("metadata")
-+            ud = guestinfo_envvar("userdata")
-+            vd = guestinfo_envvar("vendordata")
-+
-+            if md or ud or vd:
-+                self.data_access_method = DATA_ACCESS_METHOD_ENVVAR
-+
-+        # At this point, all additional data transports are valid only on
-+        # a VMware platform.
-+        if not self.data_access_method:
-+            system_type = dmi.read_dmi_data("system-product-name")
-+            if system_type is None:
-+                LOG.debug("No system-product-name found")
-+                return False
-+            if "vmware" not in system_type.lower():
-+                LOG.debug("Not a VMware platform")
-+                return False
-+
-+        # If no data was detected, check the guestinfo transport next.
-+        if not self.data_access_method:
-+            if self.vmware_rpctool:
-+                md = guestinfo("metadata", self.vmware_rpctool)
-+                ud = guestinfo("userdata", self.vmware_rpctool)
-+                vd = guestinfo("vendordata", self.vmware_rpctool)
-+
-+                if md or ud or vd:
-+                    self.data_access_method = DATA_ACCESS_METHOD_GUESTINFO
-+
-+        if not self.data_access_method:
-+            LOG.error("failed to find a valid data access method")
-+            return False
-+
-+        LOG.info("using data access method %s", self._get_subplatform())
-+
-+        # Get the metadata.
-+        self.metadata = process_metadata(load_json_or_yaml(md))
-+
-+        # Get the user data.
-+        self.userdata_raw = ud
-+
-+        # Get the vendor data.
-+        self.vendordata_raw = vd
-+
-+        # Redact any sensitive information.
-+        self.redact_keys()
-+
-+        # get_data returns true if there is any available metadata,
-+        # userdata, or vendordata.
-+        if self.metadata or self.userdata_raw or self.vendordata_raw:
-+            return True
-+        else:
-+            return False
-+
-+    def setup(self, is_new_instance):
-+        """setup(is_new_instance)
-+
-+        This is called before user-data and vendor-data have been processed.
-+
-+        Unless the datasource has set mode to 'local', then networking
-+        per 'fallback' or per 'network_config' will have been written and
-+        brought up the OS at this point.
-+        """
-+
-+        host_info = wait_on_network(self.metadata)
-+        LOG.info("got host-info: %s", host_info)
-+
-+        # Reflect any possible local IPv4 or IPv6 addresses in the guest
-+        # info.
-+        advertise_local_ip_addrs(host_info)
-+
-+        # Ensure the metadata gets updated with information about the
-+        # host, including the network interfaces, default IP addresses,
-+        # etc.
-+        self.metadata = util.mergemanydict([self.metadata, host_info])
-+
-+        # Persist the instance data for versions of cloud-init that support
-+        # doing so. This occurs here rather than in the get_data call in
-+        # order to ensure that the network interfaces are up and can be
-+        # persisted with the metadata.
-+        self.persist_instance_data()
-+
-+    def _get_subplatform(self):
-+        get_key_name_fn = None
-+        if self.data_access_method == DATA_ACCESS_METHOD_ENVVAR:
-+            get_key_name_fn = get_guestinfo_envvar_key_name
-+        elif self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO:
-+            get_key_name_fn = get_guestinfo_key_name
-+        else:
-+            return sources.METADATA_UNKNOWN
-+
-+        return "%s (%s)" % (
-+            self.data_access_method,
-+            get_key_name_fn("metadata"),
-+        )
-+
-+    @property
-+    def network_config(self):
-+        if "network" in self.metadata:
-+            LOG.debug("using metadata network config")
-+        else:
-+            LOG.debug("using fallback network config")
-+            self.metadata["network"] = {
-+                "config": self.distro.generate_fallback_config(),
-+            }
-+        return self.metadata["network"]["config"]
-+
-+    def get_instance_id(self):
-+        # Pull the instance ID out of the metadata if present. Otherwise
-+        # read the file /sys/class/dmi/id/product_uuid for the instance ID.
-+        if self.metadata and "instance-id" in self.metadata:
-+            return self.metadata["instance-id"]
-+        with open(PRODUCT_UUID_FILE_PATH, "r") as id_file:
-+            self.metadata["instance-id"] = str(id_file.read()).rstrip().lower()
-+            return self.metadata["instance-id"]
-+
-+    def get_public_ssh_keys(self):
-+        for key_name in (
-+            "public-keys-data",
-+            "public_keys_data",
-+            "public-keys",
-+            "public_keys",
-+        ):
-+            if key_name in self.metadata:
-+                return sources.normalize_pubkey_data(self.metadata[key_name])
-+        return []
-+
-+    def redact_keys(self):
-+        # Determine if there are any keys to redact.
-+        keys_to_redact = None
-+        if REDACT in self.metadata:
-+            keys_to_redact = self.metadata[REDACT]
-+        elif CLEANUP_GUESTINFO in self.metadata:
-+            # This is for backwards compatibility.
-+            keys_to_redact = self.metadata[CLEANUP_GUESTINFO]
-+
-+        if self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO:
-+            guestinfo_redact_keys(keys_to_redact, self.vmware_rpctool)
-+
-+
-+def decode(key, enc_type, data):
-+    """
-+    decode returns the decoded string value of data
-+    key is a string used to identify the data being decoded in log messages
-+    """
-+    LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
-+
-+    raw_data = None
-+    if enc_type in ["gzip+base64", "gz+b64"]:
-+        LOG.debug("Decoding %s format %s", enc_type, key)
-+        raw_data = util.decomp_gzip(util.b64d(data))
-+    elif enc_type in ["base64", "b64"]:
-+        LOG.debug("Decoding %s format %s", enc_type, key)
-+        raw_data = util.b64d(data)
-+    else:
-+        LOG.debug("Plain-text data %s", key)
-+        raw_data = data
-+
-+    return util.decode_binary(raw_data)
-+
-+
-+def get_none_if_empty_val(val):
-+    """
-+    get_none_if_empty_val returns None if the provided value, once stripped
-+    of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
-+
-+    The return value is always a string, regardless of whether the input is
-+    a bytes class or a string.
-+    """
-+
-+    # If the provided value is a bytes class, convert it to a string to
-+    # simplify the rest of this function's logic.
-+    val = util.decode_binary(val)
-+    val = val.rstrip()
-+    if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
-+        return None
-+    return val
-+
-+
-+def advertise_local_ip_addrs(host_info):
-+    """
-+    advertise_local_ip_addrs gets the local IP address information from
-+    the provided host_info map and sets the addresses in the guestinfo
-+    namespace
-+    """
-+    if not host_info:
-+        return
-+
-+    # Reflect any possible local IPv4 or IPv6 addresses in the guest
-+    # info.
-+    local_ipv4 = host_info.get(LOCAL_IPV4)
-+    if local_ipv4:
-+        guestinfo_set_value(LOCAL_IPV4, local_ipv4)
-+        LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
-+
-+    local_ipv6 = host_info.get(LOCAL_IPV6)
-+    if local_ipv6:
-+        guestinfo_set_value(LOCAL_IPV6, local_ipv6)
-+        LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
-+
-+
-+def handle_returned_guestinfo_val(key, val):
-+    """
-+    handle_returned_guestinfo_val returns the provided value if it is
-+    not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
-+    returned
-+    """
-+    val = get_none_if_empty_val(val)
-+    if val:
-+        return val
-+    LOG.debug("No value found for key %s", key)
-+    return None
-+
-+
-+def get_guestinfo_key_name(key):
-+    return "guestinfo." + key
-+
-+
-+def get_guestinfo_envvar_key_name(key):
-+    return ("vmx." + get_guestinfo_key_name(key)).upper().replace(".", "_", -1)
-+
-+
-+def guestinfo_envvar(key):
-+    val = guestinfo_envvar_get_value(key)
-+    if not val:
-+        return None
-+    enc_type = guestinfo_envvar_get_value(key + ".encoding")
-+    return decode(get_guestinfo_envvar_key_name(key), enc_type, val)
-+
-+
-+def guestinfo_envvar_get_value(key):
-+    env_key = get_guestinfo_envvar_key_name(key)
-+    return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
-+
-+
-+def guestinfo(key, vmware_rpctool=VMWARE_RPCTOOL):
-+    """
-+    guestinfo returns the guestinfo value for the provided key, decoding
-+    the value when required
-+    """
-+    val = guestinfo_get_value(key, vmware_rpctool)
-+    if not val:
-+        return None
-+    enc_type = guestinfo_get_value(key + ".encoding", vmware_rpctool)
-+    return decode(get_guestinfo_key_name(key), enc_type, val)
-+
-+
-+def guestinfo_get_value(key, vmware_rpctool=VMWARE_RPCTOOL):
-+    """
-+    Returns a guestinfo value for the specified key.
-+    """
-+    LOG.debug("Getting guestinfo value for key %s", key)
-+
-+    try:
-+        (stdout, stderr) = subp(
-+            [
-+                vmware_rpctool,
-+                "info-get " + get_guestinfo_key_name(key),
-+            ]
-+        )
-+        if stderr == NOVAL:
-+            LOG.debug("No value found for key %s", key)
-+        elif not stdout:
-+            LOG.error("Failed to get guestinfo value for key %s", key)
-+        return handle_returned_guestinfo_val(key, stdout)
-+    except ProcessExecutionError as error:
-+        if error.stderr == NOVAL:
-+            LOG.debug("No value found for key %s", key)
-+        else:
-+            util.logexc(
-+                LOG,
-+                "Failed to get guestinfo value for key %s: %s",
-+                key,
-+                error,
-+            )
-+    except Exception:
-+        util.logexc(
-+            LOG,
-+            "Unexpected error while trying to get "
-+            + "guestinfo value for key %s",
-+            key,
-+        )
-+
-+    return None
-+
-+
-+def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL):
-+    """
-+    Sets a guestinfo value for the specified key. Set value to an empty string
-+    to clear an existing guestinfo key.
-+    """
-+
-+    # If value is an empty string then set it to a single space as it is not
-+    # possible to set a guestinfo key to an empty string. Setting a guestinfo
-+    # key to a single space is as close as it gets to clearing an existing
-+    # guestinfo key.
-+    if value == "":
-+        value = " "
-+
-+    LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
-+
-+    try:
-+        subp(
-+            [
-+                vmware_rpctool,
-+                ("info-set %s %s" % (get_guestinfo_key_name(key), value)),
-+            ]
-+        )
-+        return True
-+    except ProcessExecutionError as error:
-+        util.logexc(
-+            LOG,
-+            "Failed to set guestinfo key=%s to value=%s: %s",
-+            key,
-+            value,
-+            error,
-+        )
-+    except Exception:
-+        util.logexc(
-+            LOG,
-+            "Unexpected error while trying to set "
-+            + "guestinfo key=%s to value=%s",
-+            key,
-+            value,
-+        )
-+
-+    return None
-+
-+
-+def guestinfo_redact_keys(keys, vmware_rpctool=VMWARE_RPCTOOL):
-+    """
-+    guestinfo_redact_keys redacts guestinfo of all of the keys in the given
-+    list. each key will have its value set to "---". Since the value is valid
-+    YAML, cloud-init can still read it if it tries.
-+    """
-+    if not keys:
-+        return
-+    if not type(keys) in (list, tuple):
-+        keys = [keys]
-+    for key in keys:
-+        key_name = get_guestinfo_key_name(key)
-+        LOG.info("clearing %s", key_name)
-+        if not guestinfo_set_value(
-+            key, GUESTINFO_EMPTY_YAML_VAL, vmware_rpctool
-+        ):
-+            LOG.error("failed to clear %s", key_name)
-+        LOG.info("clearing %s.encoding", key_name)
-+        if not guestinfo_set_value(key + ".encoding", "", vmware_rpctool):
-+            LOG.error("failed to clear %s.encoding", key_name)
-+
-+
-+def load_json_or_yaml(data):
-+    """
-+    load first attempts to unmarshal the provided data as JSON, and if
-+    that fails then attempts to unmarshal the data as YAML. If data is
-+    None then a new dictionary is returned.
-+    """
-+    if not data:
-+        return {}
-+    try:
-+        return util.load_json(data)
-+    except (json.JSONDecodeError, TypeError):
-+        return util.load_yaml(data)
-+
-+
-+def process_metadata(data):
-+    """
-+    process_metadata processes metadata and loads the optional network
-+    configuration.
-+    """
-+    network = None
-+    if "network" in data:
-+        network = data["network"]
-+        del data["network"]
-+
-+    network_enc = None
-+    if "network.encoding" in data:
-+        network_enc = data["network.encoding"]
-+        del data["network.encoding"]
-+
-+    if network:
-+        if isinstance(network, collections.abc.Mapping):
-+            LOG.debug("network data copied to 'config' key")
-+            network = {"config": copy.deepcopy(network)}
-+        else:
-+            LOG.debug("network data to be decoded %s", network)
-+            dec_net = decode("metadata.network", network_enc, network)
-+            network = {
-+                "config": load_json_or_yaml(dec_net),
-+            }
-+
-+        LOG.debug("network data %s", network)
-+        data["network"] = network
-+
-+    return data
-+
-+
-+# Used to match classes to dependencies
-+datasources = [
-+    (DataSourceVMware, (sources.DEP_FILESYSTEM,)),  # Run at init-local
-+    (DataSourceVMware, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
-+]
-+
-+
-+def get_datasource_list(depends):
-+    """
-+    Return a list of data sources that match this set of dependencies
-+    """
-+    return sources.list_from_depends(depends, datasources)
-+
-+
-+def get_default_ip_addrs():
-+    """
-+    Returns the default IPv4 and IPv6 addresses based on the device(s) used for
-+    the default route. Please note that None may be returned for either address
-+    family if that family has no default route or if there are multiple
-+    addresses associated with the device used by the default route for a given
-+    address.
-+    """
-+    # TODO(promote and use netifaces in cloudinit.net* modules)
-+    gateways = netifaces.gateways()
-+    if "default" not in gateways:
-+        return None, None
-+
-+    default_gw = gateways["default"]
-+    if (
-+        netifaces.AF_INET not in default_gw
-+        and netifaces.AF_INET6 not in default_gw
-+    ):
-+        return None, None
-+
-+    ipv4 = None
-+    ipv6 = None
-+
-+    gw4 = default_gw.get(netifaces.AF_INET)
-+    if gw4:
-+        _, dev4 = gw4
-+        addr4_fams = netifaces.ifaddresses(dev4)
-+        if addr4_fams:
-+            af_inet4 = addr4_fams.get(netifaces.AF_INET)
-+            if af_inet4:
-+                if len(af_inet4) > 1:
-+                    LOG.warning(
-+                        "device %s has more than one ipv4 address: %s",
-+                        dev4,
-+                        af_inet4,
-+                    )
-+                elif "addr" in af_inet4[0]:
-+                    ipv4 = af_inet4[0]["addr"]
-+
-+    # Try to get the default IPv6 address by first seeing if there is a default
-+    # IPv6 route.
-+    gw6 = default_gw.get(netifaces.AF_INET6)
-+    if gw6:
-+        _, dev6 = gw6
-+        addr6_fams = netifaces.ifaddresses(dev6)
-+        if addr6_fams:
-+            af_inet6 = addr6_fams.get(netifaces.AF_INET6)
-+            if af_inet6:
-+                if len(af_inet6) > 1:
-+                    LOG.warning(
-+                        "device %s has more than one ipv6 address: %s",
-+                        dev6,
-+                        af_inet6,
-+                    )
-+                elif "addr" in af_inet6[0]:
-+                    ipv6 = af_inet6[0]["addr"]
-+
-+    # If there is a default IPv4 address but not IPv6, then see if there is a
-+    # single IPv6 address associated with the same device associated with the
-+    # default IPv4 address.
-+    if ipv4 and not ipv6:
-+        af_inet6 = addr4_fams.get(netifaces.AF_INET6)
-+        if af_inet6:
-+            if len(af_inet6) > 1:
-+                LOG.warning(
-+                    "device %s has more than one ipv6 address: %s",
-+                    dev4,
-+                    af_inet6,
-+                )
-+            elif "addr" in af_inet6[0]:
-+                ipv6 = af_inet6[0]["addr"]
-+
-+    # If there is a default IPv6 address but not IPv4, then see if there is a
-+    # single IPv4 address associated with the same device associated with the
-+    # default IPv6 address.
-+    if not ipv4 and ipv6:
-+        af_inet4 = addr6_fams.get(netifaces.AF_INET)
-+        if af_inet4:
-+            if len(af_inet4) > 1:
-+                LOG.warning(
-+                    "device %s has more than one ipv4 address: %s",
-+                    dev6,
-+                    af_inet4,
-+                )
-+            elif "addr" in af_inet4[0]:
-+                ipv4 = af_inet4[0]["addr"]
-+
-+    return ipv4, ipv6
-+
-+
-+# patched socket.getfqdn() - see https://bugs.python.org/issue5004
-+
-+
-+def getfqdn(name=""):
-+    """Get fully qualified domain name from name.
-+    An empty argument is interpreted as meaning the local host.
-+    """
-+    # TODO(may want to promote this function to util.getfqdn)
-+    # TODO(may want to extend util.get_hostname to accept fqdn=True param)
-+    name = name.strip()
-+    if not name or name == "0.0.0.0":
-+        name = util.get_hostname()
-+    try:
-+        addrs = socket.getaddrinfo(
-+            name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME
-+        )
-+    except socket.error:
-+        pass
-+    else:
-+        for addr in addrs:
-+            if addr[3]:
-+                name = addr[3]
-+                break
-+    return name
-+
-+
-+def is_valid_ip_addr(val):
-+    """
-+    Returns false if the address is loopback, link local or unspecified;
-+    otherwise true is returned.
-+    """
-+    # TODO(extend cloudinit.net.is_ip_addr exclude link_local/loopback etc)
-+    # TODO(migrate to use cloudinit.net.is_ip_addr)#
-+
-+    addr = None
-+    try:
-+        addr = ipaddress.ip_address(val)
-+    except ipaddress.AddressValueError:
-+        addr = ipaddress.ip_address(str(val))
-+    except Exception:
-+        return None
-+
-+    if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
-+        return False
-+    return True
-+
-+
-+def get_host_info():
-+    """
-+    Returns host information such as the host name and network interfaces.
-+    """
-+    # TODO(look to promote netifices use up in cloud-init netinfo funcs)
-+    host_info = {
-+        "network": {
-+            "interfaces": {
-+                "by-mac": collections.OrderedDict(),
-+                "by-ipv4": collections.OrderedDict(),
-+                "by-ipv6": collections.OrderedDict(),
-+            },
-+        },
-+    }
-+    hostname = getfqdn(util.get_hostname())
-+    if hostname:
-+        host_info["hostname"] = hostname
-+        host_info["local-hostname"] = hostname
-+        host_info["local_hostname"] = hostname
-+
-+    default_ipv4, default_ipv6 = get_default_ip_addrs()
-+    if default_ipv4:
-+        host_info[LOCAL_IPV4] = default_ipv4
-+    if default_ipv6:
-+        host_info[LOCAL_IPV6] = default_ipv6
-+
-+    by_mac = host_info["network"]["interfaces"]["by-mac"]
-+    by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"]
-+    by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"]
-+
-+    ifaces = netifaces.interfaces()
-+    for dev_name in ifaces:
-+        addr_fams = netifaces.ifaddresses(dev_name)
-+        af_link = addr_fams.get(netifaces.AF_LINK)
-+        af_inet4 = addr_fams.get(netifaces.AF_INET)
-+        af_inet6 = addr_fams.get(netifaces.AF_INET6)
-+
-+        mac = None
-+        if af_link and "addr" in af_link[0]:
-+            mac = af_link[0]["addr"]
-+
-+        # Do not bother recording localhost
-+        if mac == "00:00:00:00:00:00":
-+            continue
-+
-+        if mac and (af_inet4 or af_inet6):
-+            key = mac
-+            val = {}
-+            if af_inet4:
-+                af_inet4_vals = []
-+                for ip_info in af_inet4:
-+                    if not is_valid_ip_addr(ip_info["addr"]):
-+                        continue
-+                    af_inet4_vals.append(ip_info)
-+                val["ipv4"] = af_inet4_vals
-+            if af_inet6:
-+                af_inet6_vals = []
-+                for ip_info in af_inet6:
-+                    if not is_valid_ip_addr(ip_info["addr"]):
-+                        continue
-+                    af_inet6_vals.append(ip_info)
-+                val["ipv6"] = af_inet6_vals
-+            by_mac[key] = val
-+
-+        if af_inet4:
-+            for ip_info in af_inet4:
-+                key = ip_info["addr"]
-+                if not is_valid_ip_addr(key):
-+                    continue
-+                val = copy.deepcopy(ip_info)
-+                del val["addr"]
-+                if mac:
-+                    val["mac"] = mac
-+                by_ipv4[key] = val
-+
-+        if af_inet6:
-+            for ip_info in af_inet6:
-+                key = ip_info["addr"]
-+                if not is_valid_ip_addr(key):
-+                    continue
-+                val = copy.deepcopy(ip_info)
-+                del val["addr"]
-+                if mac:
-+                    val["mac"] = mac
-+                by_ipv6[key] = val
-+
-+    return host_info
-+
-+
-+def wait_on_network(metadata):
-+    # Determine whether we need to wait on the network coming online.
-+    wait_on_ipv4 = False
-+    wait_on_ipv6 = False
-+    if WAIT_ON_NETWORK in metadata:
-+        wait_on_network = metadata[WAIT_ON_NETWORK]
-+        if WAIT_ON_NETWORK_IPV4 in wait_on_network:
-+            wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
-+            if isinstance(wait_on_ipv4_val, bool):
-+                wait_on_ipv4 = wait_on_ipv4_val
-+            else:
-+                wait_on_ipv4 = util.translate_bool(wait_on_ipv4_val)
-+        if WAIT_ON_NETWORK_IPV6 in wait_on_network:
-+            wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
-+            if isinstance(wait_on_ipv6_val, bool):
-+                wait_on_ipv6 = wait_on_ipv6_val
-+            else:
-+                wait_on_ipv6 = util.translate_bool(wait_on_ipv6_val)
-+
-+    # Get information about the host.
-+    host_info = None
-+    while host_info is None:
-+        # This loop + sleep results in two logs every second while waiting
-+        # for either ipv4 or ipv6 up. Do we really need to log each iteration
-+        # or can we log once and log on successful exit?
-+        host_info = get_host_info()
-+
-+        network = host_info.get("network") or {}
-+        interfaces = network.get("interfaces") or {}
-+        by_ipv4 = interfaces.get("by-ipv4") or {}
-+        by_ipv6 = interfaces.get("by-ipv6") or {}
-+
-+        if wait_on_ipv4:
-+            ipv4_ready = len(by_ipv4) > 0 if by_ipv4 else False
-+            if not ipv4_ready:
-+                host_info = None
-+
-+        if wait_on_ipv6:
-+            ipv6_ready = len(by_ipv6) > 0 if by_ipv6 else False
-+            if not ipv6_ready:
-+                host_info = None
-+
-+        if host_info is None:
-+            LOG.debug(
-+                "waiting on network: wait4=%s, ready4=%s, wait6=%s, ready6=%s",
-+                wait_on_ipv4,
-+                ipv4_ready,
-+                wait_on_ipv6,
-+                ipv6_ready,
-+            )
-+            time.sleep(1)
-+
-+    LOG.debug("waiting on network complete")
-+    return host_info
-+
-+
-+def main():
-+    """
-+    Executed when this file is used as a program.
-+    """
-+    try:
-+        logging.setupBasicLogging()
-+    except Exception:
-+        pass
-+    metadata = {
-+        "wait-on-network": {"ipv4": True, "ipv6": "false"},
-+        "network": {"config": {"dhcp": True}},
-+    }
-+    host_info = wait_on_network(metadata)
-+    metadata = util.mergemanydict([metadata, host_info])
-+    print(util.json_dumps(metadata))
-+
-+
-+if __name__ == "__main__":
-+    main()
-+
-+# vi: ts=4 expandtab
-diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst
-index f58b2b38..6606367c 100644
---- a/doc/rtd/topics/availability.rst
-+++ b/doc/rtd/topics/availability.rst
-@@ -64,5 +64,6 @@ Additionally, cloud-init is supported on these private clouds:
- - LXD
- - KVM
- - Metal-as-a-Service (MAAS)
-+- VMware
- 
- .. vi: textwidth=79
-diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst
-index 228173d2..8afed470 100644
---- a/doc/rtd/topics/datasources.rst
-+++ b/doc/rtd/topics/datasources.rst
-@@ -49,7 +49,7 @@ The following is a list of documents for each supported datasource:
-    datasources/smartos.rst
-    datasources/upcloud.rst
-    datasources/zstack.rst
--
-+   datasources/vmware.rst
- 
- Creation
- ========
-diff --git a/doc/rtd/topics/datasources/vmware.rst b/doc/rtd/topics/datasources/vmware.rst
-new file mode 100644
-index 00000000..996eb61f
---- /dev/null
-+++ b/doc/rtd/topics/datasources/vmware.rst
-@@ -0,0 +1,359 @@
-+.. _datasource_vmware:
-+
-+VMware
-+======
-+
-+This datasource is for use with systems running on a VMware platform such as
-+vSphere and currently supports the following data transports:
-+
-+
-+* `GuestInfo <https://github.com/vmware/govmomi/blob/master/govc/USAGE.md#vmchange>`_ keys
-+
-+Configuration
-+-------------
-+
-+The configuration method is dependent upon the transport:
-+
-+GuestInfo Keys
-+^^^^^^^^^^^^^^
-+
-+One method of providing meta, user, and vendor data is by setting the following
-+key/value pairs on a VM's ``extraConfig`` `property <https://vdc-repo.vmware.com/vmwb-repository/dcr-public/723e7f8b-4f21-448b-a830-5f22fd931b01/5a8257bd-7f41-4423-9a73-03307535bd42/doc/vim.vm.ConfigInfo.html>`_ :
-+
-+.. list-table::
-+   :header-rows: 1
-+
-+   * - Property
-+     - Description
-+   * - ``guestinfo.metadata``
-+     - A YAML or JSON document containing the cloud-init metadata.
-+   * - ``guestinfo.metadata.encoding``
-+     - The encoding type for ``guestinfo.metadata``.
-+   * - ``guestinfo.userdata``
-+     - A YAML document containing the cloud-init user data.
-+   * - ``guestinfo.userdata.encoding``
-+     - The encoding type for ``guestinfo.userdata``.
-+   * - ``guestinfo.vendordata``
-+     - A YAML document containing the cloud-init vendor data.
-+   * - ``guestinfo.vendordata.encoding``
-+     - The encoding type for ``guestinfo.vendordata``.
-+
-+
-+All ``guestinfo.*.encoding`` values may be set to ``base64`` or
-+``gzip+base64``.
-+
-+Features
-+--------
-+
-+This section reviews several features available in this datasource, regardless
-+of how the meta, user, and vendor data was discovered.
-+
-+Instance data and lazy networks
-+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-+
-+One of the hallmarks of cloud-init is `its use of instance-data and JINJA
-+queries <../instancedata.html#using-instance-data>`_
-+-- the ability to write queries in user and vendor data that reference runtime
-+information present in ``/run/cloud-init/instance-data.json``. This works well
-+when the metadata provides all of the information up front, such as the network
-+configuration. For systems that rely on DHCP, however, this information may not
-+be available when the metadata is persisted to disk.
-+
-+This datasource ensures that even if the instance is using DHCP to configure
-+networking, the same details about the configured network are available in
-+``/run/cloud-init/instance-data.json`` as if static networking was used. This
-+information collected at runtime is easy to demonstrate by executing the
-+datasource on the command line. From the root of this repository, run the
-+following command:
-+
-+.. code-block:: bash
-+
-+   PYTHONPATH="$(pwd)" python3 cloudinit/sources/DataSourceVMware.py
-+
-+The above command will result in output similar to the below JSON:
-+
-+.. code-block:: json
-+
-+   {
-+       "hostname": "akutz.localhost",
-+       "local-hostname": "akutz.localhost",
-+       "local-ipv4": "192.168.0.188",
-+       "local_hostname": "akutz.localhost",
-+       "network": {
-+           "config": {
-+               "dhcp": true
-+           },
-+           "interfaces": {
-+               "by-ipv4": {
-+                   "172.0.0.2": {
-+                       "netmask": "255.255.255.255",
-+                       "peer": "172.0.0.2"
-+                   },
-+                   "192.168.0.188": {
-+                       "broadcast": "192.168.0.255",
-+                       "mac": "64:4b:f0:18:9a:21",
-+                       "netmask": "255.255.255.0"
-+                   }
-+               },
-+               "by-ipv6": {
-+                   "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2": {
-+                       "flags": 208,
-+                       "mac": "64:4b:f0:18:9a:21",
-+                       "netmask": "ffff:ffff:ffff:ffff::/64"
-+                   }
-+               },
-+               "by-mac": {
-+                   "64:4b:f0:18:9a:21": {
-+                       "ipv4": [
-+                           {
-+                               "addr": "192.168.0.188",
-+                               "broadcast": "192.168.0.255",
-+                               "netmask": "255.255.255.0"
-+                           }
-+                       ],
-+                       "ipv6": [
-+                           {
-+                               "addr": "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2",
-+                               "flags": 208,
-+                               "netmask": "ffff:ffff:ffff:ffff::/64"
-+                           }
-+                       ]
-+                   },
-+                   "ac:de:48:00:11:22": {
-+                       "ipv6": []
-+                   }
-+               }
-+           }
-+       },
-+       "wait-on-network": {
-+           "ipv4": true,
-+           "ipv6": "false"
-+       }
-+   }
-+
-+
-+Redacting sensitive information
-+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-+
-+Sometimes the cloud-init userdata might contain sensitive information, and it
-+may be desirable to have the ``guestinfo.userdata`` key (or other guestinfo
-+keys) redacted as soon as its data is read by the datasource. This is possible
-+by adding the following to the metadata:
-+
-+.. code-block:: yaml
-+
-+   redact: # formerly named cleanup-guestinfo, which will also work
-+   - userdata
-+   - vendordata
-+
-+When the above snippet is added to the metadata, the datasource will iterate
-+over the elements in the ``redact`` array and clear each of the keys. For
-+example, when the guestinfo transport is used, the above snippet will cause
-+the following commands to be executed:
-+
-+.. code-block:: shell
-+
-+   vmware-rpctool "info-set guestinfo.userdata ---"
-+   vmware-rpctool "info-set guestinfo.userdata.encoding  "
-+   vmware-rpctool "info-set guestinfo.vendordata ---"
-+   vmware-rpctool "info-set guestinfo.vendordata.encoding  "
-+
-+Please note that keys are set to the valid YAML string ``---`` as it is not
-+possible remove an existing key from the guestinfo key-space. A key's analogous
-+encoding property will be set to a single white-space character, causing the
-+datasource to treat the actual key value as plain-text, thereby loading it as
-+an empty YAML doc (hence the aforementioned ``---``\ ).
-+
-+Reading the local IP addresses
-+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-+
-+This datasource automatically discovers the local IPv4 and IPv6 addresses for
-+a guest operating system based on the default routes. However, when inspecting
-+a VM externally, it's not possible to know what the *default* IP address is for
-+the guest OS. That's why this datasource sets the discovered, local IPv4 and
-+IPv6 addresses back in the guestinfo namespace as the following keys:
-+
-+
-+* ``guestinfo.local-ipv4``
-+* ``guestinfo.local-ipv6``
-+
-+It is possible that a host may not have any default, local IP addresses. It's
-+also possible the reported, local addresses are link-local addresses. But these
-+two keys may be used to discover what this datasource determined were the local
-+IPv4 and IPv6 addresses for a host.
-+
-+Waiting on the network
-+^^^^^^^^^^^^^^^^^^^^^^
-+
-+Sometimes cloud-init may bring up the network, but it will not finish coming
-+online before the datasource's ``setup`` function is called, resulting in an
-+``/var/run/cloud-init/instance-data.json`` file that does not have the correct
-+network information. It is possible to instruct the datasource to wait until an
-+IPv4 or IPv6 address is available before writing the instance data with the
-+following metadata properties:
-+
-+.. code-block:: yaml
-+
-+   wait-on-network:
-+     ipv4: true
-+     ipv6: true
-+
-+If either of the above values are true, then the datasource will sleep for a
-+second, check the network status, and repeat until one or both addresses from
-+the specified families are available.
-+
-+Walkthrough
-+-----------
-+
-+The following series of steps is a demonstration on how to configure a VM with
-+this datasource:
-+
-+
-+#. Create the metadata file for the VM. Save the following YAML to a file named
-+   ``metadata.yaml``\ :
-+
-+   .. code-block:: yaml
-+
-+       instance-id: cloud-vm
-+       local-hostname: cloud-vm
-+       network:
-+         version: 2
-+         ethernets:
-+           nics:
-+             match:
-+               name: ens*
-+             dhcp4: yes
-+
-+#. Create the userdata file ``userdata.yaml``\ :
-+
-+   .. code-block:: yaml
-+
-+       #cloud-config
-+
-+       users:
-+       - default
-+       - name: akutz
-+           primary_group: akutz
-+           sudo: ALL=(ALL) NOPASSWD:ALL
-+           groups: sudo, wheel
-+           ssh_import_id: None
-+           lock_passwd: true
-+           ssh_authorized_keys:
-+           - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDE0c5FczvcGSh/tG4iw+Fhfi/O5/EvUM/96js65tly4++YTXK1d9jcznPS5ruDlbIZ30oveCBd3kT8LLVFwzh6hepYTf0YmCTpF4eDunyqmpCXDvVscQYRXyasEm5olGmVe05RrCJSeSShAeptv4ueIn40kZKOghinGWLDSZG4+FFfgrmcMCpx5YSCtX2gvnEYZJr0czt4rxOZuuP7PkJKgC/mt2PcPjooeX00vAj81jjU2f3XKrjjz2u2+KIt9eba+vOQ6HiC8c2IzRkUAJ5i1atLy8RIbejo23+0P4N2jjk17QySFOVHwPBDTYb0/0M/4ideeU74EN/CgVsvO6JrLsPBR4dojkV5qNbMNxIVv5cUwIy2ThlLgqpNCeFIDLCWNZEFKlEuNeSQ2mPtIO7ETxEL2Cz5y/7AIuildzYMc6wi2bofRC8HmQ7rMXRWdwLKWsR0L7SKjHblIwarxOGqLnUI+k2E71YoP7SZSlxaKi17pqkr0OMCF+kKqvcvHAQuwGqyumTEWOlH6TCx1dSPrW+pVCZSHSJtSTfDW2uzL6y8k10MT06+pVunSrWo5LHAXcS91htHV1M1UrH/tZKSpjYtjMb5+RonfhaFRNzvj7cCE1f3Kp8UVqAdcGBTtReoE8eRUT63qIxjw03a7VwAyB2w+9cu1R9/vAo8SBeRqw== sakutz@gmail.com
-+
-+#. Please note this step requires that the VM be powered off. All of the
-+   commands below use the VMware CLI tool, `govc <https://github.com/vmware/govmomi/blob/master/govc>`_.
-+
-+   Go ahead and assign the path to the VM to the environment variable ``VM``\ :
-+
-+   .. code-block:: shell
-+
-+      export VM="/inventory/path/to/the/vm"
-+
-+#. Power off the VM:
-+
-+   .. raw:: html
-+
-+      <hr />
-+
-+      &#x26a0;&#xfe0f; <strong>First Boot Mode</strong>
-+
-+   To ensure the next power-on operation results in a first-boot scenario for
-+   cloud-init, it may be necessary to run the following command just before
-+   powering off the VM:
-+
-+   .. code-block:: bash
-+
-+      cloud-init clean
-+
-+   Otherwise cloud-init may not run in first-boot mode. For more information
-+   on how the boot mode is determined, please see the
-+   `First Boot Documentation <../boot.html#first-boot-determination>`_.
-+
-+   .. raw:: html
-+
-+      <hr />
-+
-+   .. code-block:: shell
-+
-+      govc vm.power -off "${VM}"
-+
-+#.
-+   Export the environment variables that contain the cloud-init metadata and
-+   userdata:
-+
-+   .. code-block:: shell
-+
-+      export METADATA=$(gzip -c9 <metadata.yaml | { base64 -w0 2>/dev/null || base64; }) \
-+           USERDATA=$(gzip -c9 <userdata.yaml | { base64 -w0 2>/dev/null || base64; })
-+
-+#.
-+   Assign the metadata and userdata to the VM:
-+
-+   .. code-block:: shell
-+
-+       govc vm.change -vm "${VM}" \
-+       -e guestinfo.metadata="${METADATA}" \
-+       -e guestinfo.metadata.encoding="gzip+base64" \
-+       -e guestinfo.userdata="${USERDATA}" \
-+       -e guestinfo.userdata.encoding="gzip+base64"
-+
-+   Please note the above commands include specifying the encoding for the
-+   properties. This is important as it informs the datasource how to decode
-+   the data for cloud-init. Valid values for ``metadata.encoding`` and
-+   ``userdata.encoding`` include:
-+
-+
-+   * ``base64``
-+   * ``gzip+base64``
-+
-+#.
-+   Power on the VM:
-+
-+   .. code-block:: shell
-+
-+       govc vm.power -vm "${VM}" -on
-+
-+If all went according to plan, the CentOS box is:
-+
-+* Locked down, allowing SSH access only for the user in the userdata
-+* Configured for a dynamic IP address via DHCP
-+* Has a hostname of ``cloud-vm``
-+
-+Examples
-+--------
-+
-+This section reviews common configurations:
-+
-+Setting the hostname
-+^^^^^^^^^^^^^^^^^^^^
-+
-+The hostname is set by way of the metadata key ``local-hostname``.
-+
-+Setting the instance ID
-+^^^^^^^^^^^^^^^^^^^^^^^
-+
-+The instance ID may be set by way of the metadata key ``instance-id``. However,
-+if this value is absent then then the instance ID is read from the file
-+``/sys/class/dmi/id/product_uuid``.
-+
-+Providing public SSH keys
-+^^^^^^^^^^^^^^^^^^^^^^^^^
-+
-+The public SSH keys may be set by way of the metadata key ``public-keys-data``.
-+Each newline-terminated string will be interpreted as a separate SSH public
-+key, which will be placed in distro's default user's
-+``~/.ssh/authorized_keys``. If the value is empty or absent, then nothing will
-+be written to ``~/.ssh/authorized_keys``.
-+
-+Configuring the network
-+^^^^^^^^^^^^^^^^^^^^^^^
-+
-+The network is configured by setting the metadata key ``network`` with a value
-+consistent with Network Config Versions
-+`1 <../network-config-format-v1.html>`_ or
-+`2 <../network-config-format-v2.html>`_\ , depending on the Linux
-+distro's version of cloud-init.
-+
-+The metadata key ``network.encoding`` may be used to indicate the format of
-+the metadata key "network". Valid encodings are ``base64`` and ``gzip+base64``.
-diff --git a/requirements.txt b/requirements.txt
-index 5817da3b..41d01d62 100644
---- a/requirements.txt
-+++ b/requirements.txt
-@@ -32,3 +32,12 @@ jsonpatch
- 
- # For validating cloud-config sections per schema definitions
- jsonschema
-+
-+# Used by DataSourceVMware to inspect the host's network configuration during
-+# the "setup()" function.
-+#
-+# This allows a host that uses DHCP to bring up the network during BootLocal
-+# and still participate in instance-data by gathering the network in detail at
-+# runtime and merge that information into the metadata and repersist that to
-+# disk.
-+netifaces>=0.10.9
-diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
-index 5912f7ee..475a2cf8 100644
---- a/tests/unittests/test_datasource/test_common.py
-+++ b/tests/unittests/test_datasource/test_common.py
-@@ -28,6 +28,7 @@ from cloudinit.sources import (
-     DataSourceScaleway as Scaleway,
-     DataSourceSmartOS as SmartOS,
-     DataSourceUpCloud as UpCloud,
-+    DataSourceVMware as VMware,
- )
- from cloudinit.sources import DataSourceNone as DSNone
- 
-@@ -50,6 +51,7 @@ DEFAULT_LOCAL = [
-     RbxCloud.DataSourceRbxCloud,
-     Scaleway.DataSourceScaleway,
-     UpCloud.DataSourceUpCloudLocal,
-+    VMware.DataSourceVMware,
- ]
- 
- DEFAULT_NETWORK = [
-@@ -66,6 +68,7 @@ DEFAULT_NETWORK = [
-     OpenStack.DataSourceOpenStack,
-     OVF.DataSourceOVFNet,
-     UpCloud.DataSourceUpCloud,
-+    VMware.DataSourceVMware,
- ]
- 
- 
-diff --git a/tests/unittests/test_datasource/test_vmware.py b/tests/unittests/test_datasource/test_vmware.py
-new file mode 100644
-index 00000000..597db7c8
---- /dev/null
-+++ b/tests/unittests/test_datasource/test_vmware.py
-@@ -0,0 +1,377 @@
-+# Copyright (c) 2021 VMware, Inc. All Rights Reserved.
-+#
-+# Authors: Andrew Kutz <akutz@vmware.com>
-+#
-+# This file is part of cloud-init. See LICENSE file for license information.
-+
-+import base64
-+import gzip
-+from cloudinit import dmi, helpers, safeyaml
-+from cloudinit import settings
-+from cloudinit.sources import DataSourceVMware
-+from cloudinit.tests.helpers import (
-+    mock,
-+    CiTestCase,
-+    FilesystemMockingTestCase,
-+    populate_dir,
-+)
-+
-+import os
-+
-+PRODUCT_NAME_FILE_PATH = "/sys/class/dmi/id/product_name"
-+PRODUCT_NAME = "VMware7,1"
-+PRODUCT_UUID = "82343CED-E4C7-423B-8F6B-0D34D19067AB"
-+REROOT_FILES = {
-+    DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
-+    PRODUCT_NAME_FILE_PATH: PRODUCT_NAME,
-+}
-+
-+VMW_MULTIPLE_KEYS = [
-+    "ssh-rsa AAAAB3NzaC1yc2EAAAA... test1@vmw.com",
-+    "ssh-rsa AAAAB3NzaC1yc2EAAAA... test2@vmw.com",
-+]
-+VMW_SINGLE_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAA... test@vmw.com"
-+
-+VMW_METADATA_YAML = """instance-id: cloud-vm
-+local-hostname: cloud-vm
-+network:
-+  version: 2
-+  ethernets:
-+    nics:
-+      match:
-+        name: ens*
-+      dhcp4: yes
-+"""
-+
-+VMW_USERDATA_YAML = """## template: jinja
-+#cloud-config
-+users:
-+- default
-+"""
-+
-+VMW_VENDORDATA_YAML = """## template: jinja
-+#cloud-config
-+runcmd:
-+- echo "Hello, world."
-+"""
-+
-+
-+class TestDataSourceVMware(CiTestCase):
-+    """
-+    Test common functionality that is not transport specific.
-+    """
-+
-+    def setUp(self):
-+        super(TestDataSourceVMware, self).setUp()
-+        self.tmp = self.tmp_dir()
-+
-+    def test_no_data_access_method(self):
-+        ds = get_ds(self.tmp)
-+        ds.vmware_rpctool = None
-+        ret = ds.get_data()
-+        self.assertFalse(ret)
-+
-+    def test_get_host_info(self):
-+        host_info = DataSourceVMware.get_host_info()
-+        self.assertTrue(host_info)
-+        self.assertTrue(host_info["hostname"])
-+        self.assertTrue(host_info["local-hostname"])
-+        self.assertTrue(host_info["local_hostname"])
-+        self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4])
-+
-+
-+class TestDataSourceVMwareEnvVars(FilesystemMockingTestCase):
-+    """
-+    Test the envvar transport.
-+    """
-+
-+    def setUp(self):
-+        super(TestDataSourceVMwareEnvVars, self).setUp()
-+        self.tmp = self.tmp_dir()
-+        os.environ[DataSourceVMware.VMX_GUESTINFO] = "1"
-+        self.create_system_files()
-+
-+    def tearDown(self):
-+        del os.environ[DataSourceVMware.VMX_GUESTINFO]
-+        return super(TestDataSourceVMwareEnvVars, self).tearDown()
-+
-+    def create_system_files(self):
-+        rootd = self.tmp_dir()
-+        populate_dir(
-+            rootd,
-+            {
-+                DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
-+            },
-+        )
-+        self.assertTrue(self.reRoot(rootd))
-+
-+    def assert_get_data_ok(self, m_fn, m_fn_call_count=6):
-+        ds = get_ds(self.tmp)
-+        ds.vmware_rpctool = None
-+        ret = ds.get_data()
-+        self.assertTrue(ret)
-+        self.assertEqual(m_fn_call_count, m_fn.call_count)
-+        self.assertEqual(
-+            ds.data_access_method, DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR
-+        )
-+        return ds
-+
-+    def assert_metadata(self, metadata, m_fn, m_fn_call_count=6):
-+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count)
-+        assert_metadata(self, ds, metadata)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_subplatform(self, m_fn):
-+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
-+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+        self.assertEqual(
-+            ds.subplatform,
-+            "%s (%s)"
-+            % (
-+                DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR,
-+                DataSourceVMware.get_guestinfo_envvar_key_name("metadata"),
-+            ),
-+        )
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_metadata_only(self, m_fn):
-+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_userdata_only(self, m_fn):
-+        m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_vendordata_only(self, m_fn):
-+        m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_metadata_base64(self, m_fn):
-+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
-+        m_fn.side_effect = [data, "base64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_metadata_b64(self, m_fn):
-+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
-+        m_fn.side_effect = [data, "b64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_metadata_gzip_base64(self, m_fn):
-+        data = VMW_METADATA_YAML.encode("utf-8")
-+        data = gzip.compress(data)
-+        data = base64.b64encode(data)
-+        m_fn.side_effect = [data, "gzip+base64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_get_data_metadata_gz_b64(self, m_fn):
-+        data = VMW_METADATA_YAML.encode("utf-8")
-+        data = gzip.compress(data)
-+        data = base64.b64encode(data)
-+        m_fn.side_effect = [data, "gz+b64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_metadata_single_ssh_key(self, m_fn):
-+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
-+        metadata["public_keys"] = VMW_SINGLE_KEY
-+        metadata_yaml = safeyaml.dumps(metadata)
-+        m_fn.side_effect = [metadata_yaml, "", "", ""]
-+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
-+
-+    @mock.patch(
-+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
-+    )
-+    def test_metadata_multiple_ssh_keys(self, m_fn):
-+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
-+        metadata["public_keys"] = VMW_MULTIPLE_KEYS
-+        metadata_yaml = safeyaml.dumps(metadata)
-+        m_fn.side_effect = [metadata_yaml, "", "", ""]
-+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
-+
-+
-+class TestDataSourceVMwareGuestInfo(FilesystemMockingTestCase):
-+    """
-+    Test the guestinfo transport on a VMware platform.
-+    """
-+
-+    def setUp(self):
-+        super(TestDataSourceVMwareGuestInfo, self).setUp()
-+        self.tmp = self.tmp_dir()
-+        self.create_system_files()
-+
-+    def create_system_files(self):
-+        rootd = self.tmp_dir()
-+        populate_dir(
-+            rootd,
-+            {
-+                DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
-+                PRODUCT_NAME_FILE_PATH: PRODUCT_NAME,
-+            },
-+        )
-+        self.assertTrue(self.reRoot(rootd))
-+
-+    def assert_get_data_ok(self, m_fn, m_fn_call_count=6):
-+        ds = get_ds(self.tmp)
-+        ds.vmware_rpctool = "vmware-rpctool"
-+        ret = ds.get_data()
-+        self.assertTrue(ret)
-+        self.assertEqual(m_fn_call_count, m_fn.call_count)
-+        self.assertEqual(
-+            ds.data_access_method,
-+            DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO,
-+        )
-+        return ds
-+
-+    def assert_metadata(self, metadata, m_fn, m_fn_call_count=6):
-+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count)
-+        assert_metadata(self, ds, metadata)
-+
-+    def test_ds_valid_on_vmware_platform(self):
-+        system_type = dmi.read_dmi_data("system-product-name")
-+        self.assertEqual(system_type, PRODUCT_NAME)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_subplatform(self, m_fn):
-+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
-+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+        self.assertEqual(
-+            ds.subplatform,
-+            "%s (%s)"
-+            % (
-+                DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO,
-+                DataSourceVMware.get_guestinfo_key_name("metadata"),
-+            ),
-+        )
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_data_userdata_only(self, m_fn):
-+        m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_data_vendordata_only(self, m_fn):
-+        m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_metadata_single_ssh_key(self, m_fn):
-+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
-+        metadata["public_keys"] = VMW_SINGLE_KEY
-+        metadata_yaml = safeyaml.dumps(metadata)
-+        m_fn.side_effect = [metadata_yaml, "", "", ""]
-+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_metadata_multiple_ssh_keys(self, m_fn):
-+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
-+        metadata["public_keys"] = VMW_MULTIPLE_KEYS
-+        metadata_yaml = safeyaml.dumps(metadata)
-+        m_fn.side_effect = [metadata_yaml, "", "", ""]
-+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_data_metadata_base64(self, m_fn):
-+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
-+        m_fn.side_effect = [data, "base64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_data_metadata_b64(self, m_fn):
-+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
-+        m_fn.side_effect = [data, "b64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_data_metadata_gzip_base64(self, m_fn):
-+        data = VMW_METADATA_YAML.encode("utf-8")
-+        data = gzip.compress(data)
-+        data = base64.b64encode(data)
-+        m_fn.side_effect = [data, "gzip+base64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_get_data_metadata_gz_b64(self, m_fn):
-+        data = VMW_METADATA_YAML.encode("utf-8")
-+        data = gzip.compress(data)
-+        data = base64.b64encode(data)
-+        m_fn.side_effect = [data, "gz+b64", "", ""]
-+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
-+
-+
-+class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase):
-+    """
-+    Test the guestinfo transport on a non-VMware platform.
-+    """
-+
-+    def setUp(self):
-+        super(TestDataSourceVMwareGuestInfo_InvalidPlatform, self).setUp()
-+        self.tmp = self.tmp_dir()
-+        self.create_system_files()
-+
-+    def create_system_files(self):
-+        rootd = self.tmp_dir()
-+        populate_dir(
-+            rootd,
-+            {
-+                DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
-+            },
-+        )
-+        self.assertTrue(self.reRoot(rootd))
-+
-+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
-+    def test_ds_invalid_on_non_vmware_platform(self, m_fn):
-+        system_type = dmi.read_dmi_data("system-product-name")
-+        self.assertEqual(system_type, None)
-+
-+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
-+        ds = get_ds(self.tmp)
-+        ds.vmware_rpctool = "vmware-rpctool"
-+        ret = ds.get_data()
-+        self.assertFalse(ret)
-+
-+
-+def assert_metadata(test_obj, ds, metadata):
-+    test_obj.assertEqual(metadata.get("instance-id"), ds.get_instance_id())
-+    test_obj.assertEqual(metadata.get("local-hostname"), ds.get_hostname())
-+
-+    expected_public_keys = metadata.get("public_keys")
-+    if not isinstance(expected_public_keys, list):
-+        expected_public_keys = [expected_public_keys]
-+
-+    test_obj.assertEqual(expected_public_keys, ds.get_public_ssh_keys())
-+    test_obj.assertIsInstance(ds.get_public_ssh_keys(), list)
-+
-+
-+def get_ds(temp_dir):
-+    ds = DataSourceVMware.DataSourceVMware(
-+        settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": temp_dir})
-+    )
-+    ds.vmware_rpctool = "vmware-rpctool"
-+    return ds
-+
-+
-+# vi: ts=4 expandtab
-diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
-index 1d8aaf18..8617d7bd 100644
---- a/tests/unittests/test_ds_identify.py
-+++ b/tests/unittests/test_ds_identify.py
-@@ -649,6 +649,50 @@ class TestDsIdentify(DsIdentifyBase):
-         """EC2: bobrightbox.com in product_serial is not brightbox'"""
-         self._test_ds_not_found('Ec2-E24Cloud-negative')
- 
-+    def test_vmware_no_valid_transports(self):
-+        """VMware: no valid transports"""
-+        self._test_ds_not_found('VMware-NoValidTransports')
-+
-+    def test_vmware_envvar_no_data(self):
-+        """VMware: envvar transport no data"""
-+        self._test_ds_not_found('VMware-EnvVar-NoData')
-+
-+    def test_vmware_envvar_no_virt_id(self):
-+        """VMware: envvar transport success if no virt id"""
-+        self._test_ds_found('VMware-EnvVar-NoVirtID')
-+
-+    def test_vmware_envvar_activated_by_metadata(self):
-+        """VMware: envvar transport activated by metadata"""
-+        self._test_ds_found('VMware-EnvVar-Metadata')
-+
-+    def test_vmware_envvar_activated_by_userdata(self):
-+        """VMware: envvar transport activated by userdata"""
-+        self._test_ds_found('VMware-EnvVar-Userdata')
-+
-+    def test_vmware_envvar_activated_by_vendordata(self):
-+        """VMware: envvar transport activated by vendordata"""
-+        self._test_ds_found('VMware-EnvVar-Vendordata')
-+
-+    def test_vmware_guestinfo_no_data(self):
-+        """VMware: guestinfo transport no data"""
-+        self._test_ds_not_found('VMware-GuestInfo-NoData')
-+
-+    def test_vmware_guestinfo_no_virt_id(self):
-+        """VMware: guestinfo transport fails if no virt id"""
-+        self._test_ds_not_found('VMware-GuestInfo-NoVirtID')
-+
-+    def test_vmware_guestinfo_activated_by_metadata(self):
-+        """VMware: guestinfo transport activated by metadata"""
-+        self._test_ds_found('VMware-GuestInfo-Metadata')
-+
-+    def test_vmware_guestinfo_activated_by_userdata(self):
-+        """VMware: guestinfo transport activated by userdata"""
-+        self._test_ds_found('VMware-GuestInfo-Userdata')
-+
-+    def test_vmware_guestinfo_activated_by_vendordata(self):
-+        """VMware: guestinfo transport activated by vendordata"""
-+        self._test_ds_found('VMware-GuestInfo-Vendordata')
-+
- 
- class TestBSDNoSys(DsIdentifyBase):
-     """Test *BSD code paths
-@@ -1136,7 +1180,240 @@ VALID_CFG = {
-     'Ec2-E24Cloud-negative': {
-         'ds': 'Ec2',
-         'files': {P_SYS_VENDOR: 'e24cloudyday\n'},
--    }
-+    },
-+    'VMware-NoValidTransports': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-EnvVar-NoData': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-EnvVar-NoVirtID': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+        ],
-+    },
-+    'VMware-EnvVar-Metadata': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-EnvVar-Userdata': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-EnvVar-Vendordata': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo',
-+                'ret': 0,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
-+                'ret': 0,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-GuestInfo-NoData': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_rpctool',
-+                'ret': 0,
-+                'out': '/usr/bin/vmware-rpctool',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_metadata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-GuestInfo-NoVirtID': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_rpctool',
-+                'ret': 0,
-+                'out': '/usr/bin/vmware-rpctool',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_metadata',
-+                'ret': 0,
-+                'out': '---',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+        ],
-+    },
-+    'VMware-GuestInfo-Metadata': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_rpctool',
-+                'ret': 0,
-+                'out': '/usr/bin/vmware-rpctool',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_metadata',
-+                'ret': 0,
-+                'out': '---',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-GuestInfo-Userdata': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_rpctool',
-+                'ret': 0,
-+                'out': '/usr/bin/vmware-rpctool',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_metadata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_userdata',
-+                'ret': 0,
-+                'out': '---',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_vendordata',
-+                'ret': 1,
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
-+    'VMware-GuestInfo-Vendordata': {
-+        'ds': 'VMware',
-+        'mocks': [
-+            {
-+                'name': 'vmware_has_rpctool',
-+                'ret': 0,
-+                'out': '/usr/bin/vmware-rpctool',
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_metadata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_userdata',
-+                'ret': 1,
-+            },
-+            {
-+                'name': 'vmware_rpctool_guestinfo_vendordata',
-+                'ret': 0,
-+                'out': '---',
-+            },
-+            MOCK_VIRT_IS_VMWARE,
-+        ],
-+    },
- }
- 
- # vi: ts=4 expandtab
-diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
-index 689d7902..cbfa883c 100644
---- a/tools/.github-cla-signers
-+++ b/tools/.github-cla-signers
-@@ -1,5 +1,6 @@
- ader1990
- ajmyyra
-+akutz
- AlexBaranowski
- Aman306
- andrewbogott
-diff --git a/tools/ds-identify b/tools/ds-identify
-index 2f2486f7..c01eae3d 100755
---- a/tools/ds-identify
-+++ b/tools/ds-identify
-@@ -125,7 +125,7 @@ DI_DSNAME=""
- # be searched if there is no setting found in config.
- DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \
- CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \
--OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud"
-+OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud VMware"
- DI_DSLIST=""
- DI_MODE=""
- DI_ON_FOUND=""
-@@ -1350,6 +1350,80 @@ dscheck_IBMCloud() {
-     return ${DS_NOT_FOUND}
- }
- 
-+vmware_has_envvar_vmx_guestinfo() {
-+    [ -n "${VMX_GUESTINFO:-}" ]
-+}
-+
-+vmware_has_envvar_vmx_guestinfo_metadata() {
-+    [ -n "${VMX_GUESTINFO_METADATA:-}" ]
-+}
-+
-+vmware_has_envvar_vmx_guestinfo_userdata() {
-+    [ -n "${VMX_GUESTINFO_USERDATA:-}" ]
-+}
-+
-+vmware_has_envvar_vmx_guestinfo_vendordata() {
-+    [ -n "${VMX_GUESTINFO_VENDORDATA:-}" ]
-+}
-+
-+vmware_has_rpctool() {
-+    command -v vmware-rpctool >/dev/null 2>&1
-+}
-+
-+vmware_rpctool_guestinfo_metadata() {
-+    vmware-rpctool "info-get guestinfo.metadata"
-+}
-+
-+vmware_rpctool_guestinfo_userdata() {
-+    vmware-rpctool "info-get guestinfo.userdata"
-+}
-+
-+vmware_rpctool_guestinfo_vendordata() {
-+    vmware-rpctool "info-get guestinfo.vendordata"
-+}
-+
-+dscheck_VMware() {
-+    # Checks to see if there is valid data for the VMware datasource.
-+    # The data transports are checked in the following order:
-+    #
-+    #   * envvars
-+    #   * guestinfo
-+    #
-+    # Please note when updating this function with support for new data
-+    # transports, the order should match the order in the _get_data
-+    # function from the file DataSourceVMware.py.
-+
-+    # Check to see if running in a container and the VMware
-+    # datasource is configured via environment variables.
-+    if vmware_has_envvar_vmx_guestinfo; then
-+        if vmware_has_envvar_vmx_guestinfo_metadata || \
-+            vmware_has_envvar_vmx_guestinfo_userdata || \
-+            vmware_has_envvar_vmx_guestinfo_vendordata; then
-+            return "${DS_FOUND}"
-+        fi
-+    fi
-+
-+    # Do not proceed unless the detected platform is VMware.
-+    if [ ! "${DI_VIRT}" = "vmware" ]; then
-+        return "${DS_NOT_FOUND}"
-+    fi
-+
-+    # Do not proceed if the vmware-rpctool command is not present.
-+    if ! vmware_has_rpctool; then
-+        return "${DS_NOT_FOUND}"
-+    fi
-+
-+    # Activate the VMware datasource only if any of the fields used
-+    # by the datasource are present in the guestinfo table.
-+    if { vmware_rpctool_guestinfo_metadata || \
-+         vmware_rpctool_guestinfo_userdata || \
-+         vmware_rpctool_guestinfo_vendordata; } >/dev/null 2>&1; then
-+        return "${DS_FOUND}"
-+    fi
-+
-+    return "${DS_NOT_FOUND}"
-+}
-+
- collect_info() {
-     read_uname_info
-     read_virt
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Detect-a-Python-version-change-and-clear-the-cache-8.patch b/SOURCES/ci-Detect-a-Python-version-change-and-clear-the-cache-8.patch
deleted file mode 100644
index a691f26..0000000
--- a/SOURCES/ci-Detect-a-Python-version-change-and-clear-the-cache-8.patch
+++ /dev/null
@@ -1,180 +0,0 @@
-From b226448134b5182ba685702e7b7a486db772d956 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 4 Mar 2022 11:21:16 +0100
-Subject: [PATCH 1/2] - Detect a Python version change and clear the cache
- (#857)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 54: - Detect a Python version change and clear the cache (#857)
-RH-Commit: [1/2] c562cd802eabae9dc14079de0b26d471d2229ca8
-RH-Bugzilla: 1935826
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-commit 78e89b03ecb29e7df3181b1219a0b5f44b9d7532
-Author: Robert Schweikert <rjschwei@suse.com>
-Date:   Thu Jul 1 12:35:40 2021 -0400
-
-    - Detect a Python version change and clear the cache (#857)
-
-    summary: Clear cache when a Python version change is detected
-
-    When a distribution gets updated it is possible that the Python version
-    changes. Python makes no guarantee that pickle is consistent across
-    versions as such we need to purge the cache and start over.
-
-    Co-authored-by: James Falcon <therealfalcon@gmail.com>
-Conflicts:
-   tests/integration_tests/util.py: test is not present downstream
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/cmd/main.py                         |  30 ++++++++++
- cloudinit/cmd/tests/test_main.py              |   2 +
- .../assets/test_version_change.pkl            | Bin 0 -> 21 bytes
- .../modules/test_ssh_auth_key_fingerprints.py |   2 +-
- .../modules/test_version_change.py            |  56 ++++++++++++++++++
- 5 files changed, 89 insertions(+), 1 deletion(-)
- create mode 100644 tests/integration_tests/assets/test_version_change.pkl
- create mode 100644 tests/integration_tests/modules/test_version_change.py
-
-diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
-index baf1381f..21213a4a 100644
---- a/cloudinit/cmd/main.py
-+++ b/cloudinit/cmd/main.py
-@@ -210,6 +210,35 @@ def attempt_cmdline_url(path, network=True, cmdline=None):
-             (cmdline_name, url, path))
- 
- 
-+def purge_cache_on_python_version_change(init):
-+    """Purge the cache if python version changed on us.
-+
-+    There could be changes not represented in our cache (obj.pkl) after we
-+    upgrade to a new version of python, so at that point clear the cache
-+    """
-+    current_python_version = '%d.%d' % (
-+        sys.version_info.major, sys.version_info.minor
-+    )
-+    python_version_path = os.path.join(
-+        init.paths.get_cpath('data'), 'python-version'
-+    )
-+    if os.path.exists(python_version_path):
-+        cached_python_version = open(python_version_path).read()
-+        # The Python version has changed out from under us, anything that was
-+        # pickled previously is likely useless due to API changes.
-+        if cached_python_version != current_python_version:
-+            LOG.debug('Python version change detected. Purging cache')
-+            init.purge_cache(True)
-+            util.write_file(python_version_path, current_python_version)
-+    else:
-+        if os.path.exists(init.paths.get_ipath_cur('obj_pkl')):
-+            LOG.info(
-+                'Writing python-version file. '
-+                'Cache compatibility status is currently unknown.'
-+            )
-+        util.write_file(python_version_path, current_python_version)
-+
-+
- def main_init(name, args):
-     deps = [sources.DEP_FILESYSTEM, sources.DEP_NETWORK]
-     if args.local:
-@@ -276,6 +305,7 @@ def main_init(name, args):
-         util.logexc(LOG, "Failed to initialize, likely bad things to come!")
-     # Stage 4
-     path_helper = init.paths
-+    purge_cache_on_python_version_change(init)
-     mode = sources.DSMODE_LOCAL if args.local else sources.DSMODE_NETWORK
- 
-     if mode == sources.DSMODE_NETWORK:
-diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py
-index 78b27441..1f5975b0 100644
---- a/cloudinit/cmd/tests/test_main.py
-+++ b/cloudinit/cmd/tests/test_main.py
-@@ -17,6 +17,8 @@ myargs = namedtuple('MyArgs', 'debug files force local reporter subcommand')
- 
- 
- class TestMain(FilesystemMockingTestCase):
-+    with_logs = True
-+    allowed_subp = False
- 
-     def setUp(self):
-         super(TestMain, self).setUp()
-diff --git a/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py b/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py
-index b9b0d85e..e1946cb1 100644
---- a/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py
-+++ b/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py
-@@ -18,7 +18,7 @@ USER_DATA_SSH_AUTHKEY_DISABLE = """\
- no_ssh_fingerprints: true
- """
- 
--USER_DATA_SSH_AUTHKEY_ENABLE="""\
-+USER_DATA_SSH_AUTHKEY_ENABLE = """\
- #cloud-config
- ssh_genkeytypes:
-   - ecdsa
-diff --git a/tests/integration_tests/modules/test_version_change.py b/tests/integration_tests/modules/test_version_change.py
-new file mode 100644
-index 00000000..4e9ab63f
---- /dev/null
-+++ b/tests/integration_tests/modules/test_version_change.py
-@@ -0,0 +1,56 @@
-+from pathlib import Path
-+
-+from tests.integration_tests.instances import IntegrationInstance
-+from tests.integration_tests.util import ASSETS_DIR
-+
-+
-+PICKLE_PATH = Path('/var/lib/cloud/instance/obj.pkl')
-+TEST_PICKLE = ASSETS_DIR / 'test_version_change.pkl'
-+
-+
-+def _assert_no_pickle_problems(log):
-+    assert 'Failed loading pickled blob' not in log
-+    assert 'Traceback' not in log
-+    assert 'WARN' not in log
-+
-+
-+def test_reboot_without_version_change(client: IntegrationInstance):
-+    log = client.read_from_file('/var/log/cloud-init.log')
-+    assert 'Python version change detected' not in log
-+    assert 'Cache compatibility status is currently unknown.' not in log
-+    _assert_no_pickle_problems(log)
-+
-+    client.restart()
-+    log = client.read_from_file('/var/log/cloud-init.log')
-+    assert 'Python version change detected' not in log
-+    assert 'Could not determine Python version used to write cache' not in log
-+    _assert_no_pickle_problems(log)
-+
-+    # Now ensure that loading a bad pickle gives us problems
-+    client.push_file(TEST_PICKLE, PICKLE_PATH)
-+    client.restart()
-+    log = client.read_from_file('/var/log/cloud-init.log')
-+    assert 'Failed loading pickled blob from {}'.format(PICKLE_PATH) in log
-+
-+
-+def test_cache_purged_on_version_change(client: IntegrationInstance):
-+    # Start by pushing the invalid pickle so we'll hit an error if the
-+    # cache didn't actually get purged
-+    client.push_file(TEST_PICKLE, PICKLE_PATH)
-+    client.execute("echo '1.0' > /var/lib/cloud/data/python-version")
-+    client.restart()
-+    log = client.read_from_file('/var/log/cloud-init.log')
-+    assert 'Python version change detected. Purging cache' in log
-+    _assert_no_pickle_problems(log)
-+
-+
-+def test_log_message_on_missing_version_file(client: IntegrationInstance):
-+    # Start by pushing a pickle so we can see the log message
-+    client.push_file(TEST_PICKLE, PICKLE_PATH)
-+    client.execute("rm /var/lib/cloud/data/python-version")
-+    client.restart()
-+    log = client.read_from_file('/var/log/cloud-init.log')
-+    assert (
-+        'Writing python-version file. '
-+        'Cache compatibility status is currently unknown.'
-+    ) in log
--- 
-2.31.1
-
diff --git a/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch b/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
deleted file mode 100644
index d4ec623..0000000
--- a/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
+++ /dev/null
@@ -1,474 +0,0 @@
-From 7bd016008429f0a18393a070d88e669f3ed89caa Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 11 Feb 2022 14:37:46 +0100
-Subject: [PATCH] Fix IPv6 netmask format for sysconfig (#1215)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 48: Fix IPv6 netmask format for sysconfig (#1215)
-RH-Commit: [1/1] 4c940bbcf85dba1fce9f4acb9fc7820c0d7777f6
-RH-Bugzilla: 2046540
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-
-commit b97a30f0a05c1dea918c46ca9c05c869d15fe2d5
-Author: Harald <hjensas@redhat.com>
-Date:   Tue Feb 8 15:49:00 2022 +0100
-
-    Fix IPv6 netmask format for sysconfig (#1215)
-
-    This change converts the IPv6 netmask from the network_data.json[1]
-    format to the CIDR style, <IPv6_addr>/<prefix>.
-
-    Using an IPv6 address like ffff:ffff:ffff:ffff:: does not work with
-    NetworkManager, nor networkscripts.
-
-    NetworkManager will ignore the route, logging:
-      ifcfg-rh: ignoring invalid route at \
-        "::/:: via fd00:fd00:fd00:2::fffe dev $DEV" \
-        (/etc/sysconfig/network-scripts/route6-$DEV:3): \
-        Argument for "::/::" is not ADDR/PREFIX format
-
-    Similarly if using networkscripts, ip route fail with error:
-      Error: inet6 prefix is expected rather than \
-        "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::".
-
-    Also a bit of refactoring ...
-
-    cloudinit.net.sysconfig.Route.to_string:
-    * Move a couple of lines around to reduce repeated code.
-    * if "ADDRESS" not in key -> continute, so that the
-      code block following it can be de-indented.
-    cloudinit.net.network_state:
-    * Refactors the ipv4_mask_to_net_prefix, ipv6_mask_to_net_prefix
-      removes mask_to_net_prefix methods. Utilize ipaddress library to
-      do some of the heavy lifting.
-
-    LP: #1959148
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/net/__init__.py                     |   7 +-
- cloudinit/net/network_state.py                | 103 +++++++-----------
- cloudinit/net/sysconfig.py                    |  91 ++++++++++------
- cloudinit/sources/DataSourceOpenNebula.py     |   2 +-
- .../sources/helpers/vmware/imc/config_nic.py  |   4 +-
- tests/unittests/test_net.py                   |  78 ++++++++++++-
- 6 files changed, 176 insertions(+), 109 deletions(-)
-
-diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
-index 003efa2a..12bf64de 100644
---- a/cloudinit/net/__init__.py
-+++ b/cloudinit/net/__init__.py
-@@ -14,7 +14,7 @@ import re
- 
- from cloudinit import subp
- from cloudinit import util
--from cloudinit.net.network_state import mask_to_net_prefix
-+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
- from cloudinit.url_helper import UrlError, readurl
- 
- LOG = logging.getLogger(__name__)
-@@ -1048,10 +1048,11 @@ class EphemeralIPv4Network(object):
-                 'Cannot init network on {0} with {1}/{2} and bcast {3}'.format(
-                     interface, ip, prefix_or_mask, broadcast))
-         try:
--            self.prefix = mask_to_net_prefix(prefix_or_mask)
-+            self.prefix = ipv4_mask_to_net_prefix(prefix_or_mask)
-         except ValueError as e:
-             raise ValueError(
--                'Cannot setup network: {0}'.format(e)
-+                "Cannot setup network, invalid prefix or "
-+                "netmask: {0}".format(e)
-             ) from e
- 
-         self.connectivity_url = connectivity_url
-diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
-index e8bf9e39..2768ef94 100644
---- a/cloudinit/net/network_state.py
-+++ b/cloudinit/net/network_state.py
-@@ -6,6 +6,7 @@
- 
- import copy
- import functools
-+import ipaddress
- import logging
- import socket
- import struct
-@@ -872,12 +873,18 @@ def _normalize_net_keys(network, address_keys=()):
-         try:
-             prefix = int(maybe_prefix)
-         except ValueError:
--            # this supports input of <address>/255.255.255.0
--            prefix = mask_to_net_prefix(maybe_prefix)
--    elif netmask:
--        prefix = mask_to_net_prefix(netmask)
--    elif 'prefix' in net:
--        prefix = int(net['prefix'])
-+            if ipv6:
-+                # this supports input of ffff:ffff:ffff::
-+                prefix = ipv6_mask_to_net_prefix(maybe_prefix)
-+            else:
-+                # this supports input of 255.255.255.0
-+                prefix = ipv4_mask_to_net_prefix(maybe_prefix)
-+    elif netmask and not ipv6:
-+        prefix = ipv4_mask_to_net_prefix(netmask)
-+    elif netmask and ipv6:
-+        prefix = ipv6_mask_to_net_prefix(netmask)
-+    elif "prefix" in net:
-+        prefix = int(net["prefix"])
-     else:
-         prefix = 64 if ipv6 else 24
- 
-@@ -972,72 +979,42 @@ def ipv4_mask_to_net_prefix(mask):
-        str(24)         => 24
-        "24"            => 24
-     """
--    if isinstance(mask, int):
--        return mask
--    if isinstance(mask, str):
--        try:
--            return int(mask)
--        except ValueError:
--            pass
--    else:
--        raise TypeError("mask '%s' is not a string or int")
--
--    if '.' not in mask:
--        raise ValueError("netmask '%s' does not contain a '.'" % mask)
--
--    toks = mask.split(".")
--    if len(toks) != 4:
--        raise ValueError("netmask '%s' had only %d parts" % (mask, len(toks)))
--
--    return sum([bin(int(x)).count('1') for x in toks])
-+    return ipaddress.ip_network(f"0.0.0.0/{mask}").prefixlen
- 
- 
- def ipv6_mask_to_net_prefix(mask):
-     """Convert an ipv6 netmask (very uncommon) or prefix (64) to prefix.
- 
--    If 'mask' is an integer or string representation of one then
--    int(mask) will be returned.
-+    If the input is already an integer or a string representation of
-+    an integer, then int(mask) will be returned.
-+       "ffff:ffff:ffff::"  => 48
-+       "48"                => 48
-     """
--
--    if isinstance(mask, int):
--        return mask
--    if isinstance(mask, str):
--        try:
--            return int(mask)
--        except ValueError:
--            pass
--    else:
--        raise TypeError("mask '%s' is not a string or int")
--
--    if ':' not in mask:
--        raise ValueError("mask '%s' does not have a ':'")
--
--    bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00,
--                0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc,
--                0xfffe, 0xffff]
--    prefix = 0
--    for word in mask.split(':'):
--        if not word or int(word, 16) == 0:
--            break
--        prefix += bitCount.index(int(word, 16))
--
--    return prefix
--
--
--def mask_to_net_prefix(mask):
--    """Return the network prefix for the netmask provided.
--
--    Supports ipv4 or ipv6 netmasks."""
-     try:
--        # if 'mask' is a prefix that is an integer.
--        # then just return it.
--        return int(mask)
-+        # In the case the mask is already a prefix
-+        prefixlen = ipaddress.ip_network(f"::/{mask}").prefixlen
-+        return prefixlen
-     except ValueError:
-+        # ValueError means mask is an IPv6 address representation and need
-+        # conversion.
-         pass
--    if is_ipv6_addr(mask):
--        return ipv6_mask_to_net_prefix(mask)
--    else:
--        return ipv4_mask_to_net_prefix(mask)
-+
-+    netmask = ipaddress.ip_address(mask)
-+    mask_int = int(netmask)
-+    # If the mask is all zeroes, just return it
-+    if mask_int == 0:
-+        return mask_int
-+
-+    trailing_zeroes = min(
-+        ipaddress.IPV6LENGTH, (~mask_int & (mask_int - 1)).bit_length()
-+    )
-+    leading_ones = mask_int >> trailing_zeroes
-+    prefixlen = ipaddress.IPV6LENGTH - trailing_zeroes
-+    all_ones = (1 << prefixlen) - 1
-+    if leading_ones != all_ones:
-+        raise ValueError("Invalid network mask '%s'" % mask)
-+
-+    return prefixlen
- 
- 
- def mask_and_ipv4_to_bcast_addr(mask, ip):
-diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index d5440998..7ecbe1c3 100644
---- a/cloudinit/net/sysconfig.py
-+++ b/cloudinit/net/sysconfig.py
-@@ -12,6 +12,7 @@ from cloudinit import util
- from cloudinit import subp
- from cloudinit.distros.parsers import networkmanager_conf
- from cloudinit.distros.parsers import resolv_conf
-+from cloudinit.net import network_state
- 
- from . import renderer
- from .network_state import (
-@@ -171,43 +172,61 @@ class Route(ConfigMap):
-         # (because Route can contain a mix of IPv4 and IPv6)
-         reindex = -1
-         for key in sorted(self._conf.keys()):
--            if 'ADDRESS' in key:
--                index = key.replace('ADDRESS', '')
--                address_value = str(self._conf[key])
--                # only accept combinations:
--                # if proto ipv6 only display ipv6 routes
--                # if proto ipv4 only display ipv4 routes
--                # do not add ipv6 routes if proto is ipv4
--                # do not add ipv4 routes if proto is ipv6
--                # (this array will contain a mix of ipv4 and ipv6)
--                if proto == "ipv4" and not self.is_ipv6_route(address_value):
--                    netmask_value = str(self._conf['NETMASK' + index])
--                    gateway_value = str(self._conf['GATEWAY' + index])
--                    # increase IPv4 index
--                    reindex = reindex + 1
--                    buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
--                                           _quote_value(address_value)))
--                    buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
--                                           _quote_value(gateway_value)))
--                    buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
--                                           _quote_value(netmask_value)))
--                    metric_key = 'METRIC' + index
--                    if metric_key in self._conf:
--                        metric_value = str(self._conf['METRIC' + index])
--                        buf.write("%s=%s\n" % ('METRIC' + str(reindex),
--                                               _quote_value(metric_value)))
--                elif proto == "ipv6" and self.is_ipv6_route(address_value):
--                    netmask_value = str(self._conf['NETMASK' + index])
--                    gateway_value = str(self._conf['GATEWAY' + index])
--                    metric_value = (
--                        'metric ' + str(self._conf['METRIC' + index])
--                        if 'METRIC' + index in self._conf else '')
-+            if "ADDRESS" not in key:
-+                continue
-+
-+            index = key.replace("ADDRESS", "")
-+            address_value = str(self._conf[key])
-+            netmask_value = str(self._conf["NETMASK" + index])
-+            gateway_value = str(self._conf["GATEWAY" + index])
-+
-+            # only accept combinations:
-+            # if proto ipv6 only display ipv6 routes
-+            # if proto ipv4 only display ipv4 routes
-+            # do not add ipv6 routes if proto is ipv4
-+            # do not add ipv4 routes if proto is ipv6
-+            # (this array will contain a mix of ipv4 and ipv6)
-+            if proto == "ipv4" and not self.is_ipv6_route(address_value):
-+                # increase IPv4 index
-+                reindex = reindex + 1
-+                buf.write(
-+                    "%s=%s\n"
-+                    % ("ADDRESS" + str(reindex), _quote_value(address_value))
-+                )
-+                buf.write(
-+                    "%s=%s\n"
-+                    % ("GATEWAY" + str(reindex), _quote_value(gateway_value))
-+                )
-+                buf.write(
-+                    "%s=%s\n"
-+                    % ("NETMASK" + str(reindex), _quote_value(netmask_value))
-+                )
-+                metric_key = "METRIC" + index
-+                if metric_key in self._conf:
-+                    metric_value = str(self._conf["METRIC" + index])
-                     buf.write(
--                        "%s/%s via %s %s dev %s\n" % (address_value,
--                                                      netmask_value,
--                                                      gateway_value,
--                                                      metric_value,
--                                                      self._route_name))
-+                        "%s=%s\n"
-+                        % ("METRIC" + str(reindex), _quote_value(metric_value))
-+                    )
-+            elif proto == "ipv6" and self.is_ipv6_route(address_value):
-+                prefix_value = network_state.ipv6_mask_to_net_prefix(
-+                    netmask_value
-+                )
-+                metric_value = (
-+                    "metric " + str(self._conf["METRIC" + index])
-+                    if "METRIC" + index in self._conf
-+                    else ""
-+                )
-+                buf.write(
-+                    "%s/%s via %s %s dev %s\n"
-+                    % (
-+                        address_value,
-+                        prefix_value,
-+                        gateway_value,
-+                        metric_value,
-+                        self._route_name,
-+                    )
-+                )
- 
-         return buf.getvalue()
- 
-diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
-index 730ec586..e7980ab1 100644
---- a/cloudinit/sources/DataSourceOpenNebula.py
-+++ b/cloudinit/sources/DataSourceOpenNebula.py
-@@ -233,7 +233,7 @@ class OpenNebulaNetwork(object):
-             # Set IPv4 address
-             devconf['addresses'] = []
-             mask = self.get_mask(c_dev)
--            prefix = str(net.mask_to_net_prefix(mask))
-+            prefix = str(net.ipv4_mask_to_net_prefix(mask))
-             devconf['addresses'].append(
-                 self.get_ip(c_dev, mac) + '/' + prefix)
- 
-diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
-index 9cd2c0c0..3a45c67e 100644
---- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
-+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
-@@ -9,7 +9,7 @@ import logging
- import os
- import re
- 
--from cloudinit.net.network_state import mask_to_net_prefix
-+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
- from cloudinit import subp
- from cloudinit import util
- 
-@@ -180,7 +180,7 @@ class NicConfigurator(object):
-         """
-         route_list = []
- 
--        cidr = mask_to_net_prefix(netmask)
-+        cidr = ipv4_mask_to_net_prefix(netmask)
- 
-         for gateway in gateways:
-             destination = "%s/%d" % (gen_subnet(gateway, netmask), cidr)
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 14d3462f..a7f6a1f7 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -2025,10 +2025,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
-                     routes:
-                         - gateway: 2001:67c:1562:1
-                           network: 2001:67c:1
--                          netmask: ffff:ffff:0
-+                          netmask: "ffff:ffff::"
-                         - gateway: 3001:67c:1562:1
-                           network: 3001:67c:1
--                          netmask: ffff:ffff:0
-+                          netmask: "ffff:ffff::"
-                           metric: 10000
-             """),
-         'expected_netplan': textwrap.dedent("""
-@@ -2295,8 +2295,8 @@ iface bond0 inet6 static
-             'route6-bond0': textwrap.dedent("""\
-         # Created by cloud-init on instance boot automatically, do not edit.
-         #
--        2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1  dev bond0
--        3001:67c:1/ffff:ffff:0 via 3001:67c:1562:1 metric 10000 dev bond0
-+        2001:67c:1/32 via 2001:67c:1562:1  dev bond0
-+        3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0
-             """),
-             'route-bond0': textwrap.dedent("""\
-         ADDRESS0=10.1.3.0
-@@ -3088,6 +3088,76 @@ USERCTL=no
-             renderer.render_network_state(ns, target=render_dir)
-         self.assertEqual([], os.listdir(render_dir))
- 
-+    def test_invalid_network_mask_ipv6(self):
-+        net_json = {
-+            "services": [{"type": "dns", "address": "172.19.0.12"}],
-+            "networks": [
-+                {
-+                    "network_id": "public-ipv6",
-+                    "type": "ipv6",
-+                    "netmask": "",
-+                    "link": "tap1a81968a-79",
-+                    "routes": [
-+                        {
-+                            "gateway": "2001:DB8::1",
-+                            "netmask": "ff:ff:ff:ff::",
-+                            "network": "2001:DB8:1::1",
-+                        },
-+                    ],
-+                    "ip_address": "2001:DB8::10",
-+                    "id": "network1",
-+                }
-+            ],
-+            "links": [
-+                {
-+                    "ethernet_mac_address": "fa:16:3e:ed:9a:59",
-+                    "mtu": None,
-+                    "type": "bridge",
-+                    "id": "tap1a81968a-79",
-+                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
-+                },
-+            ],
-+        }
-+        macs = {"fa:16:3e:ed:9a:59": "eth0"}
-+        network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
-+        with self.assertRaises(ValueError):
-+            network_state.parse_net_config_data(network_cfg, skip_broken=False)
-+
-+    def test_invalid_network_mask_ipv4(self):
-+        net_json = {
-+            "services": [{"type": "dns", "address": "172.19.0.12"}],
-+            "networks": [
-+                {
-+                    "network_id": "public-ipv4",
-+                    "type": "ipv4",
-+                    "netmask": "",
-+                    "link": "tap1a81968a-79",
-+                    "routes": [
-+                        {
-+                            "gateway": "172.20.0.1",
-+                            "netmask": "255.234.255.0",
-+                            "network": "172.19.0.0",
-+                        },
-+                    ],
-+                    "ip_address": "172.20.0.10",
-+                    "id": "network1",
-+                }
-+            ],
-+            "links": [
-+                {
-+                    "ethernet_mac_address": "fa:16:3e:ed:9a:59",
-+                    "mtu": None,
-+                    "type": "bridge",
-+                    "id": "tap1a81968a-79",
-+                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
-+                },
-+            ],
-+        }
-+        macs = {"fa:16:3e:ed:9a:59": "eth0"}
-+        network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
-+        with self.assertRaises(ValueError):
-+            network_state.parse_net_config_data(network_cfg, skip_broken=False)
-+
-     def test_openstack_rendering_samples(self):
-         for os_sample in OS_SAMPLES:
-             render_dir = self.tmp_dir()
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Fix-MIME-policy-failure-on-python-version-upgrade-93.patch b/SOURCES/ci-Fix-MIME-policy-failure-on-python-version-upgrade-93.patch
deleted file mode 100644
index 889b8db..0000000
--- a/SOURCES/ci-Fix-MIME-policy-failure-on-python-version-upgrade-93.patch
+++ /dev/null
@@ -1,705 +0,0 @@
-From 04a4cc7b8da04ba4103118cf9d975d8e9548e0dc Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 4 Mar 2022 11:23:22 +0100
-Subject: [PATCH 2/2] Fix MIME policy failure on python version upgrade (#934)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 54: - Detect a Python version change and clear the cache (#857)
-RH-Commit: [2/2] 05fc8c52a39b5ad464ad146488703467e39d73b1
-RH-Bugzilla: 1935826
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-commit eacb0353803263934aa2ac827c37e461c87cb107
-Author: James Falcon <therealfalcon@gmail.com>
-Date:   Thu Jul 15 17:52:21 2021 -0500
-
-    Fix MIME policy failure on python version upgrade (#934)
-
-    Python 3.6 added a new `policy` attribute to `MIMEMultipart`.
-    MIMEMultipart may be part of the cached object pickle of a datasource.
-    Upgrading from an old version of python to 3.6+ will cause the
-    datasource to be invalid after pickle load.
-
-    This commit uses the upgrade framework to attempt to access the mime
-    message and fail early (thus discarding the cache) if we cannot.
-    Commit 78e89b03 should fix this issue more generally.
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/sources/__init__.py                 |  18 +
- cloudinit/stages.py                           |   2 +
- .../assets/trusty_with_mime.pkl               | 572 ++++++++++++++++++
- .../modules/test_persistence.py               |  30 +
- 4 files changed, 622 insertions(+)
- create mode 100644 tests/integration_tests/assets/trusty_with_mime.pkl
- create mode 100644 tests/integration_tests/modules/test_persistence.py
-
-diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
-index 7d74f8d9..338861e6 100644
---- a/cloudinit/sources/__init__.py
-+++ b/cloudinit/sources/__init__.py
-@@ -74,6 +74,10 @@ NetworkConfigSource = namedtuple('NetworkConfigSource',
-                                  _NETCFG_SOURCE_NAMES)(*_NETCFG_SOURCE_NAMES)
- 
- 
-+class DatasourceUnpickleUserDataError(Exception):
-+    """Raised when userdata is unable to be unpickled due to python upgrades"""
-+
-+
- class DataSourceNotFoundException(Exception):
-     pass
- 
-@@ -227,6 +231,20 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta):
-             self.vendordata2 = None
-         if not hasattr(self, 'vendordata2_raw'):
-             self.vendordata2_raw = None
-+        if hasattr(self, 'userdata') and self.userdata is not None:
-+            # If userdata stores MIME data, on < python3.6 it will be
-+            # missing the 'policy' attribute that exists on >=python3.6.
-+            # Calling str() on the userdata will attempt to access this
-+            # policy attribute. This will raise an exception, causing
-+            # the pickle load to fail, so cloud-init will discard the cache
-+            try:
-+                str(self.userdata)
-+            except AttributeError as e:
-+                LOG.debug(
-+                    "Unable to unpickle datasource: %s."
-+                    " Ignoring current cache.", e
-+                )
-+                raise DatasourceUnpickleUserDataError() from e
- 
-     def __str__(self):
-         return type_utils.obj_name(self)
-diff --git a/cloudinit/stages.py b/cloudinit/stages.py
-index 83e25dd1..e709a5cf 100644
---- a/cloudinit/stages.py
-+++ b/cloudinit/stages.py
-@@ -980,6 +980,8 @@ def _pkl_load(fname):
-         return None
-     try:
-         return pickle.loads(pickle_contents)
-+    except sources.DatasourceUnpickleUserDataError:
-+        return None
-     except Exception:
-         util.logexc(LOG, "Failed loading pickled blob from %s", fname)
-         return None
-diff --git a/tests/integration_tests/assets/trusty_with_mime.pkl b/tests/integration_tests/assets/trusty_with_mime.pkl
-new file mode 100644
-index 00000000..a4089ecf
---- /dev/null
-+++ b/tests/integration_tests/assets/trusty_with_mime.pkl
-@@ -0,0 +1,572 @@
-+ccopy_reg
-+_reconstructor
-+p1
-+(ccloudinit.sources.DataSourceNoCloud
-+DataSourceNoCloudNet
-+p2
-+c__builtin__
-+object
-+p3
-+NtRp4
-+(dp5
-+S'paths'
-+p6
-+g1
-+(ccloudinit.helpers
-+Paths
-+p7
-+g3
-+NtRp8
-+(dp9
-+S'lookups'
-+p10
-+(dp11
-+S'cloud_config'
-+p12
-+S'cloud-config.txt'
-+p13
-+sS'userdata'
-+p14
-+S'user-data.txt.i'
-+p15
-+sS'vendordata'
-+p16
-+S'vendor-data.txt.i'
-+p17
-+sS'userdata_raw'
-+p18
-+S'user-data.txt'
-+p19
-+sS'boothooks'
-+p20
-+g20
-+sS'scripts'
-+p21
-+g21
-+sS'sem'
-+p22
-+g22
-+sS'data'
-+p23
-+g23
-+sS'vendor_scripts'
-+p24
-+S'scripts/vendor'
-+p25
-+sS'handlers'
-+p26
-+g26
-+sS'obj_pkl'
-+p27
-+S'obj.pkl'
-+p28
-+sS'vendordata_raw'
-+p29
-+S'vendor-data.txt'
-+p30
-+sS'vendor_cloud_config'
-+p31
-+S'vendor-cloud-config.txt'
-+p32
-+ssS'template_tpl'
-+p33
-+S'/etc/cloud/templates/%s.tmpl'
-+p34
-+sS'cfgs'
-+p35
-+(dp36
-+S'cloud_dir'
-+p37
-+S'/var/lib/cloud/'
-+p38
-+sS'templates_dir'
-+p39
-+S'/etc/cloud/templates/'
-+p40
-+sS'upstart_dir'
-+p41
-+S'/etc/init/'
-+p42
-+ssS'cloud_dir'
-+p43
-+g38
-+sS'datasource'
-+p44
-+NsS'upstart_conf_d'
-+p45
-+g42
-+sS'boot_finished'
-+p46
-+S'/var/lib/cloud/instance/boot-finished'
-+p47
-+sS'instance_link'
-+p48
-+S'/var/lib/cloud/instance'
-+p49
-+sS'seed_dir'
-+p50
-+S'/var/lib/cloud/seed'
-+p51
-+sbsS'supported_seed_starts'
-+p52
-+(S'http://'
-+p53
-+S'https://'
-+p54
-+S'ftp://'
-+p55
-+tp56
-+sS'sys_cfg'
-+p57
-+(dp58
-+S'output'
-+p59
-+(dp60
-+S'all'
-+p61
-+S'| tee -a /var/log/cloud-init-output.log'
-+p62
-+ssS'users'
-+p63
-+(lp64
-+S'default'
-+p65
-+asS'def_log_file'
-+p66
-+S'/var/log/cloud-init.log'
-+p67
-+sS'cloud_final_modules'
-+p68
-+(lp69
-+S'rightscale_userdata'
-+p70
-+aS'scripts-vendor'
-+p71
-+aS'scripts-per-once'
-+p72
-+aS'scripts-per-boot'
-+p73
-+aS'scripts-per-instance'
-+p74
-+aS'scripts-user'
-+p75
-+aS'ssh-authkey-fingerprints'
-+p76
-+aS'keys-to-console'
-+p77
-+aS'phone-home'
-+p78
-+aS'final-message'
-+p79
-+aS'power-state-change'
-+p80
-+asS'disable_root'
-+p81
-+I01
-+sS'syslog_fix_perms'
-+p82
-+S'syslog:adm'
-+p83
-+sS'log_cfgs'
-+p84
-+(lp85
-+(lp86
-+S'[loggers]\nkeys=root,cloudinit\n\n[handlers]\nkeys=consoleHandler,cloudLogHandler\n\n[formatters]\nkeys=simpleFormatter,arg0Formatter\n\n[logger_root]\nlevel=DEBUG\nhandlers=consoleHandler,cloudLogHandler\n\n[logger_cloudinit]\nlevel=DEBUG\nqualname=cloudinit\nhandlers=\npropagate=1\n\n[handler_consoleHandler]\nclass=StreamHandler\nlevel=WARNING\nformatter=arg0Formatter\nargs=(sys.stderr,)\n\n[formatter_arg0Formatter]\nformat=%(asctime)s - %(filename)s[%(levelname)s]: %(message)s\n\n[formatter_simpleFormatter]\nformat=[CLOUDINIT] %(filename)s[%(levelname)s]: %(message)s\n'
-+p87
-+aS'[handler_cloudLogHandler]\nclass=handlers.SysLogHandler\nlevel=DEBUG\nformatter=simpleFormatter\nargs=("/dev/log", handlers.SysLogHandler.LOG_USER)\n'
-+p88
-+aa(lp89
-+g87
-+aS"[handler_cloudLogHandler]\nclass=FileHandler\nlevel=DEBUG\nformatter=arg0Formatter\nargs=('/var/log/cloud-init.log',)\n"
-+p90
-+aasS'cloud_init_modules'
-+p91
-+(lp92
-+S'migrator'
-+p93
-+aS'seed_random'
-+p94
-+aS'bootcmd'
-+p95
-+aS'write-files'
-+p96
-+aS'growpart'
-+p97
-+aS'resizefs'
-+p98
-+aS'set_hostname'
-+p99
-+aS'update_hostname'
-+p100
-+aS'update_etc_hosts'
-+p101
-+aS'ca-certs'
-+p102
-+aS'rsyslog'
-+p103
-+aS'users-groups'
-+p104
-+aS'ssh'
-+p105
-+asS'preserve_hostname'
-+p106
-+I00
-+sS'_log'
-+p107
-+(lp108
-+g87
-+ag90
-+ag88
-+asS'datasource_list'
-+p109
-+(lp110
-+S'NoCloud'
-+p111
-+aS'ConfigDrive'
-+p112
-+aS'OpenNebula'
-+p113
-+aS'Azure'
-+p114
-+aS'AltCloud'
-+p115
-+aS'OVF'
-+p116
-+aS'MAAS'
-+p117
-+aS'GCE'
-+p118
-+aS'OpenStack'
-+p119
-+aS'CloudSigma'
-+p120
-+aS'Ec2'
-+p121
-+aS'CloudStack'
-+p122
-+aS'SmartOS'
-+p123
-+aS'None'
-+p124
-+asS'vendor_data'
-+p125
-+(dp126
-+S'prefix'
-+p127
-+(lp128
-+sS'enabled'
-+p129
-+I01
-+ssS'cloud_config_modules'
-+p130
-+(lp131
-+S'emit_upstart'
-+p132
-+aS'disk_setup'
-+p133
-+aS'mounts'
-+p134
-+aS'ssh-import-id'
-+p135
-+aS'locale'
-+p136
-+aS'set-passwords'
-+p137
-+aS'grub-dpkg'
-+p138
-+aS'apt-pipelining'
-+p139
-+aS'apt-configure'
-+p140
-+aS'package-update-upgrade-install'
-+p141
-+aS'landscape'
-+p142
-+aS'timezone'
-+p143
-+aS'puppet'
-+p144
-+aS'chef'
-+p145
-+aS'salt-minion'
-+p146
-+aS'mcollective'
-+p147
-+aS'disable-ec2-metadata'
-+p148
-+aS'runcmd'
-+p149
-+aS'byobu'
-+p150
-+assg14
-+(iemail.mime.multipart
-+MIMEMultipart
-+p151
-+(dp152
-+S'_headers'
-+p153
-+(lp154
-+(S'Content-Type'
-+p155
-+S'multipart/mixed; boundary="===============4291038100093149247=="'
-+tp156
-+a(S'MIME-Version'
-+p157
-+S'1.0'
-+p158
-+tp159
-+a(S'Number-Attachments'
-+p160
-+S'1'
-+tp161
-+asS'_payload'
-+p162
-+(lp163
-+(iemail.mime.base
-+MIMEBase
-+p164
-+(dp165
-+g153
-+(lp166
-+(g157
-+g158
-+tp167
-+a(S'Content-Type'
-+p168
-+S'text/x-not-multipart'
-+tp169
-+a(S'Content-Disposition'
-+p170
-+S'attachment; filename="part-001"'
-+tp171
-+asg162
-+S''
-+sS'_charset'
-+p172
-+NsS'_default_type'
-+p173
-+S'text/plain'
-+p174
-+sS'preamble'
-+p175
-+NsS'defects'
-+p176
-+(lp177
-+sS'_unixfrom'
-+p178
-+NsS'epilogue'
-+p179
-+Nsbasg172
-+Nsg173
-+g174
-+sg175
-+Nsg176
-+(lp180
-+sg178
-+Nsg179
-+Nsbsg16
-+S'#cloud-config\n{}\n\n'
-+p181
-+sg18
-+S'Content-Type: multipart/mixed; boundary="===============1378281702283945349=="\nMIME-Version: 1.0\n\n--===============1378281702283945349==\nContent-Type: text/x-shellscript; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: base64\nContent-Disposition: attachment; filename="script1.sh"\n\nIyEvYmluL3NoCgplY2hvICdoaScgPiAvdmFyL3RtcC9oaQo=\n\n--===============1378281702283945349==\nContent-Type: text/x-shellscript; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: base64\nContent-Disposition: attachment; filename="script2.sh"\n\nIyEvYmluL2Jhc2gKCmVjaG8gJ2hpMicgPiAvdmFyL3RtcC9oaTIK\n\n--===============1378281702283945349==--\n\n#cloud-config\n# final_message: |\n#   This is my final message!\n#   $version\n#   $timestamp\n#   $datasource\n#   $uptime\n# updates:\n#   network:\n#     when: [\'hotplug\']\n'
-+p182
-+sg29
-+NsS'dsmode'
-+p183
-+S'net'
-+p184
-+sS'seed'
-+p185
-+S'/var/lib/cloud/seed/nocloud-net'
-+p186
-+sS'cmdline_id'
-+p187
-+S'ds=nocloud-net'
-+p188
-+sS'ud_proc'
-+p189
-+g1
-+(ccloudinit.user_data
-+UserDataProcessor
-+p190
-+g3
-+NtRp191
-+(dp192
-+g6
-+g8
-+sS'ssl_details'
-+p193
-+(dp194
-+sbsg50
-+g186
-+sS'ds_cfg'
-+p195
-+(dp196
-+sS'distro'
-+p197
-+g1
-+(ccloudinit.distros.ubuntu
-+Distro
-+p198
-+g3
-+NtRp199
-+(dp200
-+S'osfamily'
-+p201
-+S'debian'
-+p202
-+sS'_paths'
-+p203
-+g8
-+sS'name'
-+p204
-+S'ubuntu'
-+p205
-+sS'_runner'
-+p206
-+g1
-+(ccloudinit.helpers
-+Runners
-+p207
-+g3
-+NtRp208
-+(dp209
-+g6
-+g8
-+sS'sems'
-+p210
-+(dp211
-+sbsS'_cfg'
-+p212
-+(dp213
-+S'paths'
-+p214
-+(dp215
-+g37
-+g38
-+sg39
-+g40
-+sg41
-+g42
-+ssS'default_user'
-+p216
-+(dp217
-+S'shell'
-+p218
-+S'/bin/bash'
-+p219
-+sS'name'
-+p220
-+S'ubuntu'
-+p221
-+sS'sudo'
-+p222
-+(lp223
-+S'ALL=(ALL) NOPASSWD:ALL'
-+p224
-+asS'lock_passwd'
-+p225
-+I01
-+sS'gecos'
-+p226
-+S'Ubuntu'
-+p227
-+sS'groups'
-+p228
-+(lp229
-+S'adm'
-+p230
-+aS'audio'
-+p231
-+aS'cdrom'
-+p232
-+aS'dialout'
-+p233
-+aS'dip'
-+p234
-+aS'floppy'
-+p235
-+aS'netdev'
-+p236
-+aS'plugdev'
-+p237
-+aS'sudo'
-+p238
-+aS'video'
-+p239
-+assS'package_mirrors'
-+p240
-+(lp241
-+(dp242
-+S'arches'
-+p243
-+(lp244
-+S'i386'
-+p245
-+aS'amd64'
-+p246
-+asS'failsafe'
-+p247
-+(dp248
-+S'security'
-+p249
-+S'http://security.ubuntu.com/ubuntu'
-+p250
-+sS'primary'
-+p251
-+S'http://archive.ubuntu.com/ubuntu'
-+p252
-+ssS'search'
-+p253
-+(dp254
-+S'security'
-+p255
-+(lp256
-+sS'primary'
-+p257
-+(lp258
-+S'http://%(ec2_region)s.ec2.archive.ubuntu.com/ubuntu/'
-+p259
-+aS'http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/'
-+p260
-+aS'http://%(region)s.clouds.archive.ubuntu.com/ubuntu/'
-+p261
-+assa(dp262
-+S'arches'
-+p263
-+(lp264
-+S'armhf'
-+p265
-+aS'armel'
-+p266
-+aS'default'
-+p267
-+asS'failsafe'
-+p268
-+(dp269
-+S'security'
-+p270
-+S'http://ports.ubuntu.com/ubuntu-ports'
-+p271
-+sS'primary'
-+p272
-+S'http://ports.ubuntu.com/ubuntu-ports'
-+p273
-+ssasS'ssh_svcname'
-+p274
-+S'ssh'
-+p275
-+ssbsS'metadata'
-+p276
-+(dp277
-+g183
-+g184
-+sS'local-hostname'
-+p278
-+S'me'
-+p279
-+sS'instance-id'
-+p280
-+S'me'
-+p281
-+ssb.
-\ No newline at end of file
-diff --git a/tests/integration_tests/modules/test_persistence.py b/tests/integration_tests/modules/test_persistence.py
-new file mode 100644
-index 00000000..00fdeaea
---- /dev/null
-+++ b/tests/integration_tests/modules/test_persistence.py
-@@ -0,0 +1,30 @@
-+# This file is part of cloud-init. See LICENSE file for license information.
-+"""Test the behavior of loading/discarding pickle data"""
-+from pathlib import Path
-+
-+import pytest
-+
-+from tests.integration_tests.instances import IntegrationInstance
-+from tests.integration_tests.util import (
-+    ASSETS_DIR,
-+    verify_ordered_items_in_text,
-+)
-+
-+
-+PICKLE_PATH = Path('/var/lib/cloud/instance/obj.pkl')
-+TEST_PICKLE = ASSETS_DIR / 'trusty_with_mime.pkl'
-+
-+
-+@pytest.mark.lxd_container
-+def test_log_message_on_missing_version_file(client: IntegrationInstance):
-+    client.push_file(TEST_PICKLE, PICKLE_PATH)
-+    client.restart()
-+    assert client.execute('cloud-init status --wait').ok
-+    log = client.read_from_file('/var/log/cloud-init.log')
-+    verify_ordered_items_in_text([
-+        "Unable to unpickle datasource: 'MIMEMultipart' object has no "
-+        "attribute 'policy'. Ignoring current cache.",
-+        'no cache found',
-+        'Searching for local data source',
-+        'SUCCESS: found local data from DataSourceNoCloud'
-+    ], log)
--- 
-2.31.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
deleted file mode 100644
index 6a9cfcc..0000000
--- a/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch
+++ /dev/null
@@ -1,262 +0,0 @@
-From 71989367e7a634fdd2af8ef58473975e0ef60464 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Sat, 21 Aug 2021 13:53:27 +0200
-Subject: [PATCH] Fix home permissions modified by ssh module (SC-338) (#984)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 29: Fix home permissions modified by ssh module (SC-338) (#984)
-RH-Commit: [1/1] c409f2609b1d7e024eba77b55a196a4cafadd1d7 (eesposit/cloud-init)
-RH-Bugzilla: 1995840
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-TESTED: By me and QA
-BREW: 39178090
-
-Fix home permissions modified by ssh module (SC-338) (#984)
-
-commit 7d3f5d750f6111c2716143364ea33486df67c927
-Author: James Falcon <therealfalcon@gmail.com>
-Date:   Fri Aug 20 17:09:49 2021 -0500
-
-    Fix home permissions modified by ssh module (SC-338) (#984)
-
-    Fix home permissions modified by ssh module
-
-    In #956, we updated the file and directory permissions for keys not in
-    the user's home directory. We also unintentionally modified the
-    permissions within the home directory as well. These should not change,
-    and this commit changes that back.
-
-    LP: #1940233
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/ssh_util.py                         |  35 ++++-
- .../modules/test_ssh_keysfile.py              | 132 +++++++++++++++---
- 2 files changed, 146 insertions(+), 21 deletions(-)
-
-diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
-index b8a3c8f7..9ccadf09 100644
---- a/cloudinit/ssh_util.py
-+++ b/cloudinit/ssh_util.py
-@@ -321,23 +321,48 @@ def check_create_path(username, filename, strictmodes):
-         home_folder = os.path.dirname(user_pwent.pw_dir)
-         for directory in directories:
-             parent_folder += "/" + directory
--            if home_folder.startswith(parent_folder):
-+
-+            # security check, disallow symlinks in the AuthorizedKeysFile path.
-+            if os.path.islink(parent_folder):
-+                LOG.debug(
-+                    "Invalid directory. Symlink exists in path: %s",
-+                    parent_folder)
-+                return False
-+
-+            if os.path.isfile(parent_folder):
-+                LOG.debug(
-+                    "Invalid directory. File exists in path: %s",
-+                    parent_folder)
-+                return False
-+
-+            if (home_folder.startswith(parent_folder) or
-+                    parent_folder == user_pwent.pw_dir):
-                 continue
- 
--            if not os.path.isdir(parent_folder):
-+            if not os.path.exists(parent_folder):
-                 # directory does not exist, and permission so far are good:
-                 # create the directory, and make it accessible by everyone
-                 # but owned by root, as it might be used by many users.
-                 with util.SeLinuxGuard(parent_folder):
--                    os.makedirs(parent_folder, mode=0o755, exist_ok=True)
--                    util.chownbyid(parent_folder, root_pwent.pw_uid,
--                                   root_pwent.pw_gid)
-+                    mode = 0o755
-+                    uid = root_pwent.pw_uid
-+                    gid = root_pwent.pw_gid
-+                    if parent_folder.startswith(user_pwent.pw_dir):
-+                        mode = 0o700
-+                        uid = user_pwent.pw_uid
-+                        gid = user_pwent.pw_gid
-+                    os.makedirs(parent_folder, mode=mode, exist_ok=True)
-+                    util.chownbyid(parent_folder, uid, gid)
- 
-             permissions = check_permissions(username, parent_folder,
-                                             filename, False, strictmodes)
-             if not permissions:
-                 return False
- 
-+        if os.path.islink(filename) or os.path.isdir(filename):
-+            LOG.debug("%s is not a file!", filename)
-+            return False
-+
-         # check the file
-         if not os.path.exists(filename):
-             # if file does not exist: we need to create it, since the
-diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
-index f82d7649..3159feb9 100644
---- a/tests/integration_tests/modules/test_ssh_keysfile.py
-+++ b/tests/integration_tests/modules/test_ssh_keysfile.py
-@@ -10,10 +10,10 @@ TEST_USER1_KEYS = get_test_rsa_keypair('test1')
- TEST_USER2_KEYS = get_test_rsa_keypair('test2')
- TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
- 
--USERDATA = """\
-+_USERDATA = """\
- #cloud-config
- bootcmd:
-- - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
-+ - {bootcmd}
- ssh_authorized_keys:
-  - {default}
- users:
-@@ -24,27 +24,17 @@ users:
- - name: test_user2
-   ssh_authorized_keys:
-     - {user2}
--""".format(  # noqa: E501
-+""".format(
-+    bootcmd='{bootcmd}',
-     default=TEST_DEFAULT_KEYS.public_key,
-     user1=TEST_USER1_KEYS.public_key,
-     user2=TEST_USER2_KEYS.public_key,
- )
- 
- 
--@pytest.mark.ubuntu
--@pytest.mark.user_data(USERDATA)
--def test_authorized_keys(client: IntegrationInstance):
--    expected_keys = [
--        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
--         TEST_USER1_KEYS),
--        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
--         TEST_USER2_KEYS),
--        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
--         TEST_DEFAULT_KEYS),
--        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
--    ]
--
-+def common_verify(client, expected_keys):
-     for user, filename, keys in expected_keys:
-+        # Ensure key is in the key file
-         contents = client.read_from_file(filename)
-         if user in ['ubuntu', 'root']:
-             # Our personal public key gets added by pycloudlib
-@@ -83,3 +73,113 @@ def test_authorized_keys(client: IntegrationInstance):
-                     look_for_keys=False,
-                     allow_agent=False,
-                 )
-+
-+        # Ensure we haven't messed with any /home permissions
-+        # See LP: #1940233
-+        home_dir = '/home/{}'.format(user)
-+        home_perms = '755'
-+        if user == 'root':
-+            home_dir = '/root'
-+            home_perms = '700'
-+        assert '{} {}'.format(user, home_perms) == client.execute(
-+            'stat -c "%U %a" {}'.format(home_dir)
-+        )
-+        if client.execute("test -d {}/.ssh".format(home_dir)).ok:
-+            assert '{} 700'.format(user) == client.execute(
-+                'stat -c "%U %a" {}/.ssh'.format(home_dir)
-+            )
-+        assert '{} 600'.format(user) == client.execute(
-+            'stat -c "%U %a" {}'.format(filename)
-+        )
-+
-+        # Also ensure ssh-keygen works as expected
-+        client.execute('mkdir {}/.ssh'.format(home_dir))
-+        assert client.execute(
-+            "ssh-keygen -b 2048 -t rsa -f {}/.ssh/id_rsa -q -N ''".format(
-+                home_dir)
-+        ).ok
-+        assert client.execute('test -f {}/.ssh/id_rsa'.format(home_dir))
-+        assert client.execute('test -f {}/.ssh/id_rsa.pub'.format(home_dir))
-+
-+    assert 'root 755' == client.execute('stat -c "%U %a" /home')
-+
-+
-+DEFAULT_KEYS_USERDATA = _USERDATA.format(bootcmd='""')
-+
-+
-+@pytest.mark.ubuntu
-+@pytest.mark.user_data(DEFAULT_KEYS_USERDATA)
-+def test_authorized_keys_default(client: IntegrationInstance):
-+    expected_keys = [
-+        ('test_user1', '/home/test_user1/.ssh/authorized_keys',
-+         TEST_USER1_KEYS),
-+        ('test_user2', '/home/test_user2/.ssh/authorized_keys',
-+         TEST_USER2_KEYS),
-+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys',
-+         TEST_DEFAULT_KEYS),
-+        ('root', '/root/.ssh/authorized_keys', TEST_DEFAULT_KEYS),
-+    ]
-+    common_verify(client, expected_keys)
-+
-+
-+AUTHORIZED_KEYS2_USERDATA = _USERDATA.format(bootcmd=(
-+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
-+    "/etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' "
-+    "/etc/ssh/sshd_config"))
-+
-+
-+@pytest.mark.ubuntu
-+@pytest.mark.user_data(AUTHORIZED_KEYS2_USERDATA)
-+def test_authorized_keys2(client: IntegrationInstance):
-+    expected_keys = [
-+        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
-+         TEST_USER1_KEYS),
-+        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
-+         TEST_USER2_KEYS),
-+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
-+         TEST_DEFAULT_KEYS),
-+        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
-+    ]
-+    common_verify(client, expected_keys)
-+
-+
-+NESTED_KEYS_USERDATA = _USERDATA.format(bootcmd=(
-+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
-+    "/etc/ssh/authorized_keys %h/foo/bar/ssh/keys;' "
-+    "/etc/ssh/sshd_config"))
-+
-+
-+@pytest.mark.ubuntu
-+@pytest.mark.user_data(NESTED_KEYS_USERDATA)
-+def test_nested_keys(client: IntegrationInstance):
-+    expected_keys = [
-+        ('test_user1', '/home/test_user1/foo/bar/ssh/keys',
-+         TEST_USER1_KEYS),
-+        ('test_user2', '/home/test_user2/foo/bar/ssh/keys',
-+         TEST_USER2_KEYS),
-+        ('ubuntu', '/home/ubuntu/foo/bar/ssh/keys',
-+         TEST_DEFAULT_KEYS),
-+        ('root', '/root/foo/bar/ssh/keys', TEST_DEFAULT_KEYS),
-+    ]
-+    common_verify(client, expected_keys)
-+
-+
-+EXTERNAL_KEYS_USERDATA = _USERDATA.format(bootcmd=(
-+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
-+    "/etc/ssh/authorized_keys /etc/ssh/authorized_keys/%u/keys;' "
-+    "/etc/ssh/sshd_config"))
-+
-+
-+@pytest.mark.ubuntu
-+@pytest.mark.user_data(EXTERNAL_KEYS_USERDATA)
-+def test_external_keys(client: IntegrationInstance):
-+    expected_keys = [
-+        ('test_user1', '/etc/ssh/authorized_keys/test_user1/keys',
-+         TEST_USER1_KEYS),
-+        ('test_user2', '/etc/ssh/authorized_keys/test_user2/keys',
-+         TEST_USER2_KEYS),
-+        ('ubuntu', '/etc/ssh/authorized_keys/ubuntu/keys',
-+         TEST_DEFAULT_KEYS),
-+        ('root', '/etc/ssh/authorized_keys/root/keys', TEST_DEFAULT_KEYS),
-+    ]
-+    common_verify(client, expected_keys)
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch b/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch
deleted file mode 100644
index c47788f..0000000
--- a/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch
+++ /dev/null
@@ -1,47 +0,0 @@
-From 0eeec94882779de76c08b1a7faf862e22f21f242 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 14 Jan 2022 16:42:46 +0100
-Subject: [PATCH 5/6] Revert unnecesary lcase in ds-identify (#978)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 44: Datasource for VMware
-RH-Commit: [5/6] f7385c15cf17a9c4a2fa15b29afd1b8a96b24d1e
-RH-Bugzilla: 2026587
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-commit f516a7d37c1654addc02485e681b4358d7e7c0db
-Author: Andrew Kutz <101085+akutz@users.noreply.github.com>
-Date:   Fri Aug 13 14:30:55 2021 -0500
-
-    Revert unnecesary lcase in ds-identify (#978)
-
-    This patch reverts an unnecessary lcase optimization in the
-    ds-identify script. SystemD documents the values produced by
-    the systemd-detect-virt command are lower case, and the mapping
-    table used by the FreeBSD check is also lower-case.
-
-    The optimization added two new forked processes, needlessly
-    causing overhead.
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- tools/ds-identify | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/tools/ds-identify b/tools/ds-identify
-index 0e12298f..7b782462 100755
---- a/tools/ds-identify
-+++ b/tools/ds-identify
-@@ -449,7 +449,7 @@ detect_virt() {
- read_virt() {
-     cached "$DI_VIRT" && return 0
-     detect_virt
--    DI_VIRT="$(echo "${_RET}" | tr '[:upper:]' '[:lower:]')"
-+    DI_VIRT="${_RET}"
- }
- 
- is_container() {
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch
deleted file mode 100644
index e46b52b..0000000
--- a/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch
+++ /dev/null
@@ -1,1385 +0,0 @@
-From 3b68aff3b7b1dc567ef6721a269c2d4e054b729f Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Mon, 9 Aug 2021 23:41:44 +0200
-Subject: [PATCH] Stop copying ssh system keys and check folder permissions
- (#956)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 28: Stop copying ssh system keys and check folder permissions (#956)
-RH-Commit: [1/1] 7cada613be82f2f525ee56b86ef9f71edf40d2ef (eesposit/cloud-init)
-RH-Bugzilla: 1862967
-RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-TESTED: By me and QA
-BREW: 38818284
-
-This is a continuation of previous MR 25 and upstream PR #937.
-There were still issues when using non-standard file paths like
-/etc/ssh/userkeys/%u or /etc/ssh/authorized_keys, and the choice
-of storing the keys of all authorized_keys files into a single
-one was not ideal. This fix modifies cloudinit to support
-all different cases of authorized_keys file locations, and
-picks a user-specific file where to copy the new keys that
-complies with ssh permissions.
-
-commit 00dbaf1e9ab0e59d81662f0f3561897bef499a3f
-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date:   Mon Aug 9 16:49:56 2021 +0200
-
-    Stop copying ssh system keys and check folder permissions (#956)
-
-    In /etc/ssh/sshd_config, it is possible to define a custom
-    authorized_keys file that will contain the keys allowed to access the
-    machine via the AuthorizedKeysFile option. Cloudinit is able to add
-    user-specific keys to the existing ones, but we need to be careful on
-    which of the authorized_keys files listed to pick.
-    Chosing a file that is shared by all user will cause security
-    issues, because the owner of that key can then access also other users.
-
-    We therefore pick an authorized_keys file only if it satisfies the
-    following conditions:
-    1. it is not a "global" file, ie it must be defined in
-       AuthorizedKeysFile with %u, %h or be in  /home/<user>. This avoids
-       security issues.
-    2. it must comply with ssh permission requirements, otherwise the ssh
-       agent won't use that file.
-
-    If it doesn't meet either of those conditions, write to
-    ~/.ssh/authorized_keys
-
-    We also need to consider the case when the chosen authorized_keys file
-    does not exist. In this case, the existing behavior of cloud-init is
-    to create the new file. We therefore need to be sure that the file
-    complies with ssh permissions too, by setting:
-    - the actual file to permission 600, and owned by the user
-    - the directories in the path that do not exist must be root owned and
-      with permission 755.
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/ssh_util.py           | 133 ++++-
- cloudinit/util.py               |  51 +-
- tests/unittests/test_sshutil.py | 952 +++++++++++++++++++++++++-------
- 3 files changed, 920 insertions(+), 216 deletions(-)
-
-diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
-index 89057262..b8a3c8f7 100644
---- a/cloudinit/ssh_util.py
-+++ b/cloudinit/ssh_util.py
-@@ -249,6 +249,113 @@ def render_authorizedkeysfile_paths(value, homedir, username):
-     return rendered
- 
- 
-+# Inspired from safe_path() in openssh source code (misc.c).
-+def check_permissions(username, current_path, full_path, is_file, strictmodes):
-+    """Check if the file/folder in @current_path has the right permissions.
-+
-+    We need to check that:
-+    1. If StrictMode is enabled, the owner is either root or the user
-+    2. the user can access the file/folder, otherwise ssh won't use it
-+    3. If StrictMode is enabled, no write permission is given to group
-+       and world users (022)
-+    """
-+
-+    # group/world can only execute the folder (access)
-+    minimal_permissions = 0o711
-+    if is_file:
-+        # group/world can only read the file
-+        minimal_permissions = 0o644
-+
-+    # 1. owner must be either root or the user itself
-+    owner = util.get_owner(current_path)
-+    if strictmodes and owner != username and owner != "root":
-+        LOG.debug("Path %s in %s must be own by user %s or"
-+                  " by root, but instead is own by %s. Ignoring key.",
-+                  current_path, full_path, username, owner)
-+        return False
-+
-+    parent_permission = util.get_permissions(current_path)
-+    # 2. the user can access the file/folder, otherwise ssh won't use it
-+    if owner == username:
-+        # need only the owner permissions
-+        minimal_permissions &= 0o700
-+    else:
-+        group_owner = util.get_group(current_path)
-+        user_groups = util.get_user_groups(username)
-+
-+        if group_owner in user_groups:
-+            # need only the group permissions
-+            minimal_permissions &= 0o070
-+        else:
-+            # need only the world permissions
-+            minimal_permissions &= 0o007
-+
-+    if parent_permission & minimal_permissions == 0:
-+        LOG.debug("Path %s in %s must be accessible by user %s,"
-+                  " check its permissions",
-+                  current_path, full_path, username)
-+        return False
-+
-+    # 3. no write permission (w) is given to group and world users (022)
-+    # Group and world user can still have +rx.
-+    if strictmodes and parent_permission & 0o022 != 0:
-+        LOG.debug("Path %s in %s must not give write"
-+                  "permission to group or world users. Ignoring key.",
-+                  current_path, full_path)
-+        return False
-+
-+    return True
-+
-+
-+def check_create_path(username, filename, strictmodes):
-+    user_pwent = users_ssh_info(username)[1]
-+    root_pwent = users_ssh_info("root")[1]
-+    try:
-+        # check the directories first
-+        directories = filename.split("/")[1:-1]
-+
-+        # scan in order, from root to file name
-+        parent_folder = ""
-+        # this is to comply also with unit tests, and
-+        # strange home directories
-+        home_folder = os.path.dirname(user_pwent.pw_dir)
-+        for directory in directories:
-+            parent_folder += "/" + directory
-+            if home_folder.startswith(parent_folder):
-+                continue
-+
-+            if not os.path.isdir(parent_folder):
-+                # directory does not exist, and permission so far are good:
-+                # create the directory, and make it accessible by everyone
-+                # but owned by root, as it might be used by many users.
-+                with util.SeLinuxGuard(parent_folder):
-+                    os.makedirs(parent_folder, mode=0o755, exist_ok=True)
-+                    util.chownbyid(parent_folder, root_pwent.pw_uid,
-+                                   root_pwent.pw_gid)
-+
-+            permissions = check_permissions(username, parent_folder,
-+                                            filename, False, strictmodes)
-+            if not permissions:
-+                return False
-+
-+        # check the file
-+        if not os.path.exists(filename):
-+            # if file does not exist: we need to create it, since the
-+            # folders at this point exist and have right permissions
-+            util.write_file(filename, '', mode=0o600, ensure_dir_exists=True)
-+            util.chownbyid(filename, user_pwent.pw_uid, user_pwent.pw_gid)
-+
-+        permissions = check_permissions(username, filename,
-+                                        filename, True, strictmodes)
-+        if not permissions:
-+            return False
-+    except (IOError, OSError) as e:
-+        util.logexc(LOG, str(e))
-+        return False
-+
-+    return True
-+
-+
- def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
-     (ssh_dir, pw_ent) = users_ssh_info(username)
-     default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys')
-@@ -259,6 +366,7 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
-             ssh_cfg = parse_ssh_config_map(sshd_cfg_file)
-             key_paths = ssh_cfg.get("authorizedkeysfile",
-                                     "%h/.ssh/authorized_keys")
-+            strictmodes = ssh_cfg.get("strictmodes", "yes")
-             auth_key_fns = render_authorizedkeysfile_paths(
-                 key_paths, pw_ent.pw_dir, username)
- 
-@@ -269,31 +377,31 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
-                         "config from %r, using 'AuthorizedKeysFile' file "
-                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
- 
--    # check if one of the keys is the user's one
-+    # check if one of the keys is the user's one and has the right permissions
-     for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns):
-         if any([
-             '%u' in key_path,
-             '%h' in key_path,
-             auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir))
-         ]):
--            user_authorizedkeys_file = auth_key_fn
-+            permissions_ok = check_create_path(username, auth_key_fn,
-+                                               strictmodes == "yes")
-+            if permissions_ok:
-+                user_authorizedkeys_file = auth_key_fn
-+                break
- 
-     if user_authorizedkeys_file != default_authorizedkeys_file:
-         LOG.debug(
-             "AuthorizedKeysFile has an user-specific authorized_keys, "
-             "using %s", user_authorizedkeys_file)
- 
--    # always store all the keys in the user's private file
--    return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
-+    return (
-+        user_authorizedkeys_file,
-+        parse_authorized_keys([user_authorizedkeys_file])
-+    )
- 
- 
- def setup_user_keys(keys, username, options=None):
--    # Make sure the users .ssh dir is setup accordingly
--    (ssh_dir, pwent) = users_ssh_info(username)
--    if not os.path.isdir(ssh_dir):
--        util.ensure_dir(ssh_dir, mode=0o700)
--        util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
--
-     # Turn the 'update' keys given into actual entries
-     parser = AuthKeyLineParser()
-     key_entries = []
-@@ -302,11 +410,10 @@ def setup_user_keys(keys, username, options=None):
- 
-     # Extract the old and make the new
-     (auth_key_fn, auth_key_entries) = extract_authorized_keys(username)
-+    ssh_dir = os.path.dirname(auth_key_fn)
-     with util.SeLinuxGuard(ssh_dir, recursive=True):
-         content = update_authorized_keys(auth_key_entries, key_entries)
--        util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700)
--        util.write_file(auth_key_fn, content, mode=0o600)
--        util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid)
-+        util.write_file(auth_key_fn, content, preserve_mode=True)
- 
- 
- class SshdConfigLine(object):
-diff --git a/cloudinit/util.py b/cloudinit/util.py
-index 4e0a72db..343976ad 100644
---- a/cloudinit/util.py
-+++ b/cloudinit/util.py
-@@ -35,6 +35,7 @@ from base64 import b64decode, b64encode
- from errno import ENOENT
- from functools import lru_cache
- from urllib import parse
-+from typing import List
- 
- from cloudinit import importer
- from cloudinit import log as logging
-@@ -1830,6 +1831,53 @@ def chmod(path, mode):
-             os.chmod(path, real_mode)
- 
- 
-+def get_permissions(path: str) -> int:
-+    """
-+    Returns the octal permissions of the file/folder pointed by the path,
-+    encoded as an int.
-+
-+    @param path: The full path of the file/folder.
-+    """
-+
-+    return stat.S_IMODE(os.stat(path).st_mode)
-+
-+
-+def get_owner(path: str) -> str:
-+    """
-+    Returns the owner of the file/folder pointed by the path.
-+
-+    @param path: The full path of the file/folder.
-+    """
-+    st = os.stat(path)
-+    return pwd.getpwuid(st.st_uid).pw_name
-+
-+
-+def get_group(path: str) -> str:
-+    """
-+    Returns the group of the file/folder pointed by the path.
-+
-+    @param path: The full path of the file/folder.
-+    """
-+    st = os.stat(path)
-+    return grp.getgrgid(st.st_gid).gr_name
-+
-+
-+def get_user_groups(username: str) -> List[str]:
-+    """
-+    Returns a list of all groups to which the user belongs
-+
-+    @param username: the user we want to check
-+    """
-+    groups = []
-+    for group in grp.getgrall():
-+        if username in group.gr_mem:
-+            groups.append(group.gr_name)
-+
-+    gid = pwd.getpwnam(username).pw_gid
-+    groups.append(grp.getgrgid(gid).gr_name)
-+    return groups
-+
-+
- def write_file(
-     filename,
-     content,
-@@ -1856,8 +1904,7 @@ def write_file(
- 
-     if preserve_mode:
-         try:
--            file_stat = os.stat(filename)
--            mode = stat.S_IMODE(file_stat.st_mode)
-+            mode = get_permissions(filename)
-         except OSError:
-             pass
- 
-diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
-index bcb8044f..a66788bf 100644
---- a/tests/unittests/test_sshutil.py
-+++ b/tests/unittests/test_sshutil.py
-@@ -1,6 +1,9 @@
- # This file is part of cloud-init. See LICENSE file for license information.
- 
-+import os
-+
- from collections import namedtuple
-+from functools import partial
- from unittest.mock import patch
- 
- from cloudinit import ssh_util
-@@ -8,13 +11,48 @@ from cloudinit.tests import helpers as test_helpers
- from cloudinit import util
- 
- # https://stackoverflow.com/questions/11351032/
--FakePwEnt = namedtuple(
--    'FakePwEnt',
--    ['pw_dir', 'pw_gecos', 'pw_name', 'pw_passwd', 'pw_shell', 'pwd_uid'])
-+FakePwEnt = namedtuple('FakePwEnt', [
-+    'pw_name',
-+    'pw_passwd',
-+    'pw_uid',
-+    'pw_gid',
-+    'pw_gecos',
-+    'pw_dir',
-+    'pw_shell',
-+])
- FakePwEnt.__new__.__defaults__ = tuple(
-     "UNSET_%s" % n for n in FakePwEnt._fields)
- 
- 
-+def mock_get_owner(updated_permissions, value):
-+    try:
-+        return updated_permissions[value][0]
-+    except ValueError:
-+        return util.get_owner(value)
-+
-+
-+def mock_get_group(updated_permissions, value):
-+    try:
-+        return updated_permissions[value][1]
-+    except ValueError:
-+        return util.get_group(value)
-+
-+
-+def mock_get_user_groups(username):
-+    return username
-+
-+
-+def mock_get_permissions(updated_permissions, value):
-+    try:
-+        return updated_permissions[value][2]
-+    except ValueError:
-+        return util.get_permissions(value)
-+
-+
-+def mock_getpwnam(users, username):
-+    return users[username]
-+
-+
- # Do not use these public keys, most of them are fetched from
- # the testdata for OpenSSH, and their private keys are available
- # https://github.com/openssh/openssh-portable/tree/master/regress/unittests/sshkey/testdata
-@@ -552,12 +590,30 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
-             ssh_util.render_authorizedkeysfile_paths(
-                 "/opt/%u/keys", "/home/bobby", "bobby"))
- 
-+    def test_user_file(self):
-+        self.assertEqual(
-+            ["/opt/bobby"],
-+            ssh_util.render_authorizedkeysfile_paths(
-+                "/opt/%u", "/home/bobby", "bobby"))
-+
-+    def test_user_file2(self):
-+        self.assertEqual(
-+            ["/opt/bobby/bobby"],
-+            ssh_util.render_authorizedkeysfile_paths(
-+                "/opt/%u/%u", "/home/bobby", "bobby"))
-+
-     def test_multiple(self):
-         self.assertEqual(
-             ["/keys/path1", "/keys/path2"],
-             ssh_util.render_authorizedkeysfile_paths(
-                 "/keys/path1 /keys/path2", "/home/bobby", "bobby"))
- 
-+    def test_multiple2(self):
-+        self.assertEqual(
-+            ["/keys/path1", "/keys/bobby"],
-+            ssh_util.render_authorizedkeysfile_paths(
-+                "/keys/path1 /keys/%u", "/home/bobby", "bobby"))
-+
-     def test_relative(self):
-         self.assertEqual(
-             ["/home/bobby/.secret/keys"],
-@@ -581,269 +637,763 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
- 
- class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
- 
--    @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
--        m_getpwnam.return_value = fpw
--        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
--
--        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
--        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
--        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
--
--        # /tmp/home2/bobby/.ssh/user_keys = dsa
--        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
--        util.write_file(user_keys, VALID_CONTENT['dsa'])
--
--        # /tmp/sshd_config
-+    def create_fake_users(self, names, mock_permissions,
-+                          m_get_group, m_get_owner, m_get_permissions,
-+                          m_getpwnam, users):
-+        homes = []
-+
-+        root = '/tmp/root'
-+        fpw = FakePwEnt(pw_name="root", pw_dir=root)
-+        users["root"] = fpw
-+
-+        for name in names:
-+            home = '/tmp/home/' + name
-+            fpw = FakePwEnt(pw_name=name, pw_dir=home)
-+            users[name] = fpw
-+            homes.append(home)
-+
-+        m_get_permissions.side_effect = partial(
-+            mock_get_permissions, mock_permissions)
-+        m_get_owner.side_effect = partial(mock_get_owner, mock_permissions)
-+        m_get_group.side_effect = partial(mock_get_group, mock_permissions)
-+        m_getpwnam.side_effect = partial(mock_getpwnam, users)
-+        return homes
-+
-+    def create_user_authorized_file(self, home, filename, content_key, keys):
-+        user_ssh_folder = "%s/.ssh" % home
-+        # /tmp/home/<user>/.ssh/authorized_keys = content_key
-+        authorized_keys = self.tmp_path(filename, dir=user_ssh_folder)
-+        util.write_file(authorized_keys, VALID_CONTENT[content_key])
-+        keys[authorized_keys] = content_key
-+        return authorized_keys
-+
-+    def create_global_authorized_file(self, filename, content_key, keys):
-+        authorized_keys = self.tmp_path(filename, dir='/tmp')
-+        util.write_file(authorized_keys, VALID_CONTENT[content_key])
-+        keys[authorized_keys] = content_key
-+        return authorized_keys
-+
-+    def create_sshd_config(self, authorized_keys_files):
-         sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-         util.write_file(
-             sshd_config,
--            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
-+            "AuthorizedKeysFile " + authorized_keys_files
-         )
-+        return sshd_config
- 
-+    def execute_and_check(self, user, sshd_config, solution, keys,
-+                          delete_keys=True):
-         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
-+            user, sshd_config)
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
--        self.assertEqual(user_keys, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        self.assertEqual(auth_key_fn, solution)
-+        for path, key in keys.items():
-+            if path == solution:
-+                self.assertTrue(VALID_CONTENT[key] in content)
-+            else:
-+                self.assertFalse(VALID_CONTENT[key] in content)
-+
-+        if delete_keys and os.path.isdir("/tmp/home/"):
-+            util.delete_dir_contents("/tmp/home/")
- 
-     @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
--        m_getpwnam.return_value = fpw
--        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_single_user_two_local_files(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        user_bobby = 'bobby'
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+        }
-+
-+        homes = self.create_fake_users(
-+            [user_bobby], mock_permissions, m_get_group, m_get_owner,
-+            m_get_permissions, m_getpwnam, users
-+        )
-+        home = homes[0]
- 
--        # /tmp/home/suzie/.ssh/authorized_keys = rsa
--        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
--        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home, 'authorized_keys', 'rsa', keys
-+        )
- 
--        # /tmp/home/suzie/.ssh/user_keys = dsa
--        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
--        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+        # /tmp/home/bobby/.ssh/user_keys = dsa
-+        user_keys = self.create_user_authorized_file(
-+            home, 'user_keys', 'dsa', keys
-+        )
- 
-         # /tmp/sshd_config
--        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
--        util.write_file(
--            sshd_config,
--            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
-+        options = "%s %s" % (authorized_keys, user_keys)
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_single_user_two_local_files_inverted(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        user_bobby = 'bobby'
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+        }
-+
-+        homes = self.create_fake_users(
-+            [user_bobby], mock_permissions, m_get_group, m_get_owner,
-+            m_get_permissions, m_getpwnam, users
-         )
-+        home = homes[0]
- 
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home, 'authorized_keys', 'rsa', keys
-+        )
- 
--        self.assertEqual(authorized_keys, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        # /tmp/home/bobby/.ssh/user_keys = dsa
-+        user_keys = self.create_user_authorized_file(
-+            home, 'user_keys', 'dsa', keys
-+        )
- 
--    @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
--        m_getpwnam.return_value = fpw
--        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+        # /tmp/sshd_config
-+        options = "%s %s" % (user_keys, authorized_keys)
-+        sshd_config = self.create_sshd_config(options)
- 
--        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
--        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
--        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+        self.execute_and_check(user_bobby, sshd_config, user_keys, keys)
- 
--        # /tmp/home2/bobby/.ssh/user_keys = dsa
--        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
--        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_single_user_local_global_files(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        user_bobby = 'bobby'
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+        }
-+
-+        homes = self.create_fake_users(
-+            [user_bobby], mock_permissions, m_get_group, m_get_owner,
-+            m_get_permissions, m_getpwnam, users
-+        )
-+        home = homes[0]
- 
--        # /tmp/etc/ssh/authorized_keys = ecdsa
--        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
--                                               dir="/tmp")
--        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home, 'authorized_keys', 'rsa', keys
-+        )
- 
--        # /tmp/sshd_config
--        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
--        util.write_file(
--            sshd_config,
--            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
--                                             user_keys, authorized_keys)
-+        # /tmp/home/bobby/.ssh/user_keys = dsa
-+        user_keys = self.create_user_authorized_file(
-+            home, 'user_keys', 'dsa', keys
-         )
- 
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        authorized_keys_global = self.create_global_authorized_file(
-+            'etc/ssh/authorized_keys', 'ecdsa', keys
-+        )
- 
--        self.assertEqual(authorized_keys, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
--        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        options = "%s %s %s" % (authorized_keys_global, user_keys,
-+                                authorized_keys)
-+        sshd_config = self.create_sshd_config(options)
- 
--    @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
--        m_getpwnam.return_value = fpw
--        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+        self.execute_and_check(user_bobby, sshd_config, user_keys, keys)
- 
--        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
--        authorized_keys = self.tmp_path('authorized_keys2',
--                                        dir=user_ssh_folder)
--        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_single_user_local_global_files_inverted(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        user_bobby = 'bobby'
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
-+        }
-+
-+        homes = self.create_fake_users(
-+            [user_bobby], mock_permissions, m_get_group, m_get_owner,
-+            m_get_permissions, m_getpwnam, users
-+        )
-+        home = homes[0]
- 
--        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
--        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
--        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home, 'authorized_keys2', 'rsa', keys
-+        )
- 
--        # /tmp/etc/ssh/authorized_keys = ecdsa
--        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
--                                               dir="/tmp")
--        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+        # /tmp/home/bobby/.ssh/user_keys = dsa
-+        user_keys = self.create_user_authorized_file(
-+            home, 'user_keys3', 'dsa', keys
-+        )
- 
--        # /tmp/sshd_config
--        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
--        util.write_file(
--            sshd_config,
--            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
--                                             authorized_keys, user_keys)
-+        authorized_keys_global = self.create_global_authorized_file(
-+            'etc/ssh/authorized_keys', 'ecdsa', keys
-         )
- 
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        options = "%s %s %s" % (authorized_keys_global, authorized_keys,
-+                                user_keys)
-+        sshd_config = self.create_sshd_config(options)
- 
--        self.assertEqual(user_keys, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
--        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys)
- 
-     @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_global(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
--        m_getpwnam.return_value = fpw
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_single_user_global_file(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        user_bobby = 'bobby'
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+        }
-+
-+        homes = self.create_fake_users(
-+            [user_bobby], mock_permissions, m_get_group, m_get_owner,
-+            m_get_permissions, m_getpwnam, users
-+        )
-+        home = homes[0]
- 
-         # /tmp/etc/ssh/authorized_keys = rsa
--        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
--                                               dir="/tmp")
--        util.write_file(authorized_keys_global, VALID_CONTENT['rsa'])
-+        authorized_keys_global = self.create_global_authorized_file(
-+            'etc/ssh/authorized_keys', 'rsa', keys
-+        )
- 
--        # /tmp/sshd_config
--        sshd_config = self.tmp_path('sshd_config')
--        util.write_file(
--            sshd_config,
--            "AuthorizedKeysFile %s" % (authorized_keys_global)
-+        options = "%s" % authorized_keys_global
-+        sshd_config = self.create_sshd_config(options)
-+
-+        default = "%s/.ssh/authorized_keys" % home
-+        self.execute_and_check(user_bobby, sshd_config, default, keys)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_local_file_standard(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_suzie = 'suzie'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_suzie], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-         )
-+        home_bobby = homes[0]
-+        home_suzie = homes[1]
- 
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys', 'rsa', keys
-+        )
- 
--        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
-+        # /tmp/home/suzie/.ssh/authorized_keys = rsa
-+        authorized_keys2 = self.create_user_authorized_file(
-+            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
-+        )
-+
-+        options = ".ssh/authorized_keys"
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
- 
-     @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
--        m_getpwnam.return_value = fpw
--        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
--        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
--        authorized_keys = self.tmp_path('authorized_keys2',
--                                        dir=user_ssh_folder)
--        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
--        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
--        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
--        util.write_file(user_keys, VALID_CONTENT['dsa'])
--
--        fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
--        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
--        # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com
--        authorized_keys2 = self.tmp_path('authorized_keys2',
--                                         dir=user_ssh_folder)
--        util.write_file(authorized_keys2,
--                        VALID_CONTENT['ssh-xmss@openssh.com'])
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_local_file_custom(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_suzie = 'suzie'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_suzie], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        home_bobby = homes[0]
-+        home_suzie = homes[1]
- 
--        # /tmp/etc/ssh/authorized_keys = ecdsa
--        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
--                                               dir="/tmp")
--        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys2', 'rsa', keys
-+        )
- 
--        # /tmp/sshd_config
--        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
--        util.write_file(
--            sshd_config,
--            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" %
--            (authorized_keys_global, user_keys)
-+        # /tmp/home/suzie/.ssh/authorized_keys2 = rsa
-+        authorized_keys2 = self.create_user_authorized_file(
-+            home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys
-         )
- 
--        # process first user
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        options = ".ssh/authorized_keys2"
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
- 
--        self.assertEqual(user_keys, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
--        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
--        self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_local_global_files(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600),
-+            '/tmp/home/suzie/.ssh/user_keys3': ('suzie', 'suzie', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_suzie = 'suzie'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_suzie], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        home_bobby = homes[0]
-+        home_suzie = homes[1]
- 
--        m_getpwnam.return_value = fpw2
--        # process second user
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw2.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
-+        self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys2', 'rsa', keys
-+        )
-+        # /tmp/home/bobby/.ssh/user_keys3 = dsa
-+        user_keys = self.create_user_authorized_file(
-+            home_bobby, 'user_keys3', 'dsa', keys
-+        )
-+
-+        # /tmp/home/suzie/.ssh/authorized_keys2 = rsa
-+        authorized_keys2 = self.create_user_authorized_file(
-+            home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys
-+        )
-+
-+        # /tmp/etc/ssh/authorized_keys = ecdsa
-+        authorized_keys_global = self.create_global_authorized_file(
-+            'etc/ssh/authorized_keys2', 'ecdsa', keys
-+        )
-+
-+        options = "%s %s %%h/.ssh/authorized_keys2" % \
-+            (authorized_keys_global, user_keys)
-+        sshd_config = self.create_sshd_config(options)
- 
--        self.assertEqual(authorized_keys2, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
--        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
--        self.assertFalse(VALID_CONTENT['rsa'] in content)
-+        self.execute_and_check(
-+            user_bobby, sshd_config, user_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
- 
-+    @patch("cloudinit.util.get_user_groups")
-     @patch("cloudinit.ssh_util.pwd.getpwnam")
--    def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby')
--        m_getpwnam.return_value = fpw
--        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_local_global_files_badguy(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
-+        m_get_user_groups
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600),
-+            '/tmp/home/badguy': ('root', 'root', 0o755),
-+            '/tmp/home/badguy/home': ('root', 'root', 0o755),
-+            '/tmp/home/badguy/home/bobby': ('root', 'root', 0o655),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_badguy = 'badguy'
-+        home_bobby, *_ = self.create_fake_users(
-+            [user_bobby, user_badguy], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        m_get_user_groups.side_effect = mock_get_user_groups
-+
-         # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
--        authorized_keys = self.tmp_path('authorized_keys2',
--                                        dir=user_ssh_folder)
--        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+        authorized_keys = self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys2', 'rsa', keys
-+        )
-         # /tmp/home/bobby/.ssh/user_keys3 = dsa
--        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
--        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+        user_keys = self.create_user_authorized_file(
-+            home_bobby, 'user_keys3', 'dsa', keys
-+        )
- 
--        fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy')
--        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
-         # /tmp/home/badguy/home/bobby = ""
-         authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy")
-+        util.write_file(authorized_keys2, '')
- 
-         # /tmp/etc/ssh/authorized_keys = ecdsa
--        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
--                                               dir="/tmp")
--        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+        authorized_keys_global = self.create_global_authorized_file(
-+            'etc/ssh/authorized_keys2', 'ecdsa', keys
-+        )
- 
-         # /tmp/sshd_config
--        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
--        util.write_file(
--            sshd_config,
--            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" %
--            (authorized_keys_global, user_keys, authorized_keys2)
-+        options = "%s %%h/.ssh/authorized_keys2 %s %s" % \
-+            (authorized_keys2, authorized_keys_global, user_keys)
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(
-+            user_badguy, sshd_config, authorized_keys2, keys
-         )
- 
--        # process first user
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+    @patch("cloudinit.util.get_user_groups")
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_unaccessible_file(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
-+        m_get_user_groups
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+
-+            '/tmp/etc': ('root', 'root', 0o755),
-+            '/tmp/etc/ssh': ('root', 'root', 0o755),
-+            '/tmp/etc/ssh/userkeys': ('root', 'root', 0o700),
-+            '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600),
-+            '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600),
-+
-+            '/tmp/home/badguy': ('badguy', 'badguy', 0o700),
-+            '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700),
-+            '/tmp/home/badguy/.ssh/authorized_keys':
-+                ('badguy', 'badguy', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_badguy = 'badguy'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_badguy], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        m_get_user_groups.side_effect = mock_get_user_groups
-+        home_bobby = homes[0]
-+        home_badguy = homes[1]
- 
--        self.assertEqual(user_keys, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['rsa'] in content)
--        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys', 'rsa', keys
-+        )
-+        # /tmp/etc/ssh/userkeys/bobby = dsa
-+        # assume here that we can bypass userkeys, despite permissions
-+        self.create_global_authorized_file(
-+            'etc/ssh/userkeys/bobby', 'dsa', keys
-+        )
- 
--        m_getpwnam.return_value = fpw2
--        # process second user
--        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw2.pw_name, sshd_config)
--        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+        # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com
-+        authorized_keys2 = self.create_user_authorized_file(
-+            home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys
-+        )
- 
--        # badguy should not take the key from the other user!
--        self.assertEqual(authorized_keys2, auth_key_fn)
--        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
--        self.assertTrue(VALID_CONTENT['dsa'] in content)
--        self.assertFalse(VALID_CONTENT['rsa'] in content)
-+        # /tmp/etc/ssh/userkeys/badguy = ecdsa
-+        self.create_global_authorized_file(
-+            'etc/ssh/userkeys/badguy', 'ecdsa', keys
-+        )
-+
-+        # /tmp/sshd_config
-+        options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys"
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(
-+            user_badguy, sshd_config, authorized_keys2, keys
-+        )
-+
-+    @patch("cloudinit.util.get_user_groups")
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_accessible_file(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
-+        m_get_user_groups
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+
-+            '/tmp/etc': ('root', 'root', 0o755),
-+            '/tmp/etc/ssh': ('root', 'root', 0o755),
-+            '/tmp/etc/ssh/userkeys': ('root', 'root', 0o755),
-+            '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600),
-+            '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600),
-+
-+            '/tmp/home/badguy': ('badguy', 'badguy', 0o700),
-+            '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700),
-+            '/tmp/home/badguy/.ssh/authorized_keys':
-+                ('badguy', 'badguy', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_badguy = 'badguy'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_badguy], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        m_get_user_groups.side_effect = mock_get_user_groups
-+        home_bobby = homes[0]
-+        home_badguy = homes[1]
-+
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys', 'rsa', keys
-+        )
-+        # /tmp/etc/ssh/userkeys/bobby = dsa
-+        # assume here that we can bypass userkeys, despite permissions
-+        authorized_keys = self.create_global_authorized_file(
-+            'etc/ssh/userkeys/bobby', 'dsa', keys
-+        )
-+
-+        # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com
-+        self.create_user_authorized_file(
-+            home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys
-+        )
-+
-+        # /tmp/etc/ssh/userkeys/badguy = ecdsa
-+        authorized_keys2 = self.create_global_authorized_file(
-+            'etc/ssh/userkeys/badguy', 'ecdsa', keys
-+        )
-+
-+        # /tmp/sshd_config
-+        options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys"
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(
-+            user_badguy, sshd_config, authorized_keys2, keys
-+        )
-+
-+    @patch("cloudinit.util.get_user_groups")
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_hardcoded_single_user_file(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
-+        m_get_user_groups
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+
-+            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_suzie = 'suzie'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_suzie], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        home_bobby = homes[0]
-+        home_suzie = homes[1]
-+        m_get_user_groups.side_effect = mock_get_user_groups
-+
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys', 'rsa', keys
-+        )
-+
-+        # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com
-+        self.create_user_authorized_file(
-+            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
-+        )
-+
-+        # /tmp/sshd_config
-+        options = "%s" % (authorized_keys)
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        default = "%s/.ssh/authorized_keys" % home_suzie
-+        self.execute_and_check(user_suzie, sshd_config, default, keys)
-+
-+    @patch("cloudinit.util.get_user_groups")
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_hardcoded_single_user_file_inverted(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
-+        m_get_user_groups
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+
-+            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_suzie = 'suzie'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_suzie], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        home_bobby = homes[0]
-+        home_suzie = homes[1]
-+        m_get_user_groups.side_effect = mock_get_user_groups
-+
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys', 'rsa', keys
-+        )
-+
-+        # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com
-+        authorized_keys2 = self.create_user_authorized_file(
-+            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
-+        )
-+
-+        # /tmp/sshd_config
-+        options = "%s" % (authorized_keys2)
-+        sshd_config = self.create_sshd_config(options)
-+
-+        default = "%s/.ssh/authorized_keys" % home_bobby
-+        self.execute_and_check(
-+            user_bobby, sshd_config, default, keys, delete_keys=False
-+        )
-+        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
-+
-+    @patch("cloudinit.util.get_user_groups")
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    @patch("cloudinit.util.get_permissions")
-+    @patch("cloudinit.util.get_owner")
-+    @patch("cloudinit.util.get_group")
-+    def test_two_users_hardcoded_user_files(
-+        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
-+        m_get_user_groups
-+    ):
-+        keys = {}
-+        users = {}
-+        mock_permissions = {
-+            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
-+            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
-+
-+            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
-+            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
-+        }
-+
-+        user_bobby = 'bobby'
-+        user_suzie = 'suzie'
-+        homes = self.create_fake_users(
-+            [user_bobby, user_suzie], mock_permissions, m_get_group,
-+            m_get_owner, m_get_permissions, m_getpwnam, users
-+        )
-+        home_bobby = homes[0]
-+        home_suzie = homes[1]
-+        m_get_user_groups.side_effect = mock_get_user_groups
-+
-+        # /tmp/home/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.create_user_authorized_file(
-+            home_bobby, 'authorized_keys', 'rsa', keys
-+        )
-+
-+        # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com
-+        authorized_keys2 = self.create_user_authorized_file(
-+            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
-+        )
-+
-+        # /tmp/etc/ssh/authorized_keys = ecdsa
-+        authorized_keys_global = self.create_global_authorized_file(
-+            'etc/ssh/authorized_keys', 'ecdsa', keys
-+        )
-+
-+        # /tmp/sshd_config
-+        options = "%s %s %s" % \
-+            (authorized_keys_global, authorized_keys, authorized_keys2)
-+        sshd_config = self.create_sshd_config(options)
-+
-+        self.execute_and_check(
-+            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
-+        )
-+        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
- 
- # vi: ts=4 expandtab
--- 
-2.27.0
-
diff --git a/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch b/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch
deleted file mode 100644
index 07c44fe..0000000
--- a/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch
+++ /dev/null
@@ -1,97 +0,0 @@
-From ded01bd47c65636e59dc332d06fb8acb982ec677 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Fri, 14 Jan 2022 16:41:52 +0100
-Subject: [PATCH 4/6] Update dscheck_VMware's rpctool check (#970)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 44: Datasource for VMware
-RH-Commit: [4/6] 509f68596f2d8f32027677f756b9d81e6a507ff1
-RH-Bugzilla: 2026587
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-commit 7781dec3306e9467f216cfcb36b7e10a8b38547a
-Author: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com>
-Date:   Fri Aug 13 00:40:39 2021 +0530
-
-    Update dscheck_VMware's rpctool check (#970)
-
-    This patch updates the dscheck_VMware function's use of "vmware-rpctool".
-
-    When checking to see if a "guestinfo" property is set.
-    Because a successful exit code can occur even if there is an empty
-    string returned, it is possible that the VMware datasource will be
-    loaded as a false-positive. This patch ensures that in addition to
-    validating the exit code, the emitted output is also examined to ensure
-    a non-empty value is returned by rpctool before returning "${DS_FOUND}"
-    from "dscheck_VMware()".
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- tools/ds-identify | 15 +++++++++------
- 1 file changed, 9 insertions(+), 6 deletions(-)
-
-diff --git a/tools/ds-identify b/tools/ds-identify
-index c01eae3d..0e12298f 100755
---- a/tools/ds-identify
-+++ b/tools/ds-identify
-@@ -141,6 +141,7 @@ error() {
-     debug 0 "$@"
-     stderr "$@"
- }
-+
- warn() {
-     set -- "WARN:" "$@"
-     debug 0 "$@"
-@@ -344,7 +345,6 @@ geom_label_status_as() {
-     return $ret
- }
- 
--
- read_fs_info_freebsd() {
-     local oifs="$IFS" line="" delim=","
-     local ret=0 labels="" dev="" label="" ftype="" isodevs=""
-@@ -404,7 +404,6 @@ cached() {
-     [ -n "$1" ] && _RET="$1" && return || return 1
- }
- 
--
- detect_virt() {
-     local virt="${UNAVAILABLE}" r="" out=""
-     if [ -d /run/systemd ]; then
-@@ -450,7 +449,7 @@ detect_virt() {
- read_virt() {
-     cached "$DI_VIRT" && return 0
-     detect_virt
--    DI_VIRT=${_RET}
-+    DI_VIRT="$(echo "${_RET}" | tr '[:upper:]' '[:lower:]')"
- }
- 
- is_container() {
-@@ -1370,16 +1369,20 @@ vmware_has_rpctool() {
-     command -v vmware-rpctool >/dev/null 2>&1
- }
- 
-+vmware_rpctool_guestinfo() {
-+    vmware-rpctool "info-get guestinfo.${1}" 2>/dev/null | grep "[[:alnum:]]"
-+}
-+
- vmware_rpctool_guestinfo_metadata() {
--    vmware-rpctool "info-get guestinfo.metadata"
-+    vmware_rpctool_guestinfo "metadata"
- }
- 
- vmware_rpctool_guestinfo_userdata() {
--    vmware-rpctool "info-get guestinfo.userdata"
-+    vmware_rpctool_guestinfo "userdata"
- }
- 
- vmware_rpctool_guestinfo_vendordata() {
--    vmware-rpctool "info-get guestinfo.vendordata"
-+    vmware_rpctool_guestinfo "vendordata"
- }
- 
- dscheck_VMware() {
--- 
-2.27.0
-
diff --git a/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch b/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch
deleted file mode 100644
index 1ccfec9..0000000
--- a/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch
+++ /dev/null
@@ -1,470 +0,0 @@
-From 6e79106a09a0d142915da1fb48640575bb4bfe08 Mon Sep 17 00:00:00 2001
-From: Anh Vo <anhvo@microsoft.com>
-Date: Tue, 13 Apr 2021 17:39:39 -0400
-Subject: [PATCH 3/7] azure: Removing ability to invoke walinuxagent (#799)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
-RH-Commit: [3/7] f5e98665bf2093edeeccfcd95b47df2e44a40536
-RH-Bugzilla: 2023940
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-Invoking walinuxagent from within cloud-init is no longer
-supported/necessary
----
- cloudinit/sources/DataSourceAzure.py          | 137 ++++--------------
- doc/rtd/topics/datasources/azure.rst          |  62 ++------
- tests/unittests/test_datasource/test_azure.py |  97 -------------
- 3 files changed, 35 insertions(+), 261 deletions(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index de1452ce..020b7006 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -381,53 +381,6 @@ class DataSourceAzure(sources.DataSource):
-                     util.logexc(LOG, "handling set_hostname failed")
-         return False
- 
--    @azure_ds_telemetry_reporter
--    def get_metadata_from_agent(self):
--        temp_hostname = self.metadata.get('local-hostname')
--        agent_cmd = self.ds_cfg['agent_command']
--        LOG.debug("Getting metadata via agent.  hostname=%s cmd=%s",
--                  temp_hostname, agent_cmd)
--
--        self.bounce_network_with_azure_hostname()
--
--        try:
--            invoke_agent(agent_cmd)
--        except subp.ProcessExecutionError:
--            # claim the datasource even if the command failed
--            util.logexc(LOG, "agent command '%s' failed.",
--                        self.ds_cfg['agent_command'])
--
--        ddir = self.ds_cfg['data_dir']
--
--        fp_files = []
--        key_value = None
--        for pk in self.cfg.get('_pubkeys', []):
--            if pk.get('value', None):
--                key_value = pk['value']
--                LOG.debug("SSH authentication: using value from fabric")
--            else:
--                bname = str(pk['fingerprint'] + ".crt")
--                fp_files += [os.path.join(ddir, bname)]
--                LOG.debug("SSH authentication: "
--                          "using fingerprint from fabric")
--
--        with events.ReportEventStack(
--                name="waiting-for-ssh-public-key",
--                description="wait for agents to retrieve SSH keys",
--                parent=azure_ds_reporter):
--            # wait very long for public SSH keys to arrive
--            # https://bugs.launchpad.net/cloud-init/+bug/1717611
--            missing = util.log_time(logfunc=LOG.debug,
--                                    msg="waiting for SSH public key files",
--                                    func=util.wait_for_files,
--                                    args=(fp_files, 900))
--            if len(missing):
--                LOG.warning("Did not find files, but going on: %s", missing)
--
--        metadata = {}
--        metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files)
--        return metadata
--
-     def _get_subplatform(self):
-         """Return the subplatform metadata source details."""
-         if self.seed.startswith('/dev'):
-@@ -1354,35 +1307,32 @@ class DataSourceAzure(sources.DataSource):
-            On failure, returns False.
-         """
- 
--        if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:
--            self.bounce_network_with_azure_hostname()
-+        self.bounce_network_with_azure_hostname()
- 
--            pubkey_info = None
--            try:
--                raise KeyError(
--                    "Not using public SSH keys from IMDS"
--                )
--                # pylint:disable=unreachable
--                public_keys = self.metadata['imds']['compute']['publicKeys']
--                LOG.debug(
--                    'Successfully retrieved %s key(s) from IMDS',
--                    len(public_keys)
--                    if public_keys is not None
--                    else 0
--                )
--            except KeyError:
--                LOG.debug(
--                    'Unable to retrieve SSH keys from IMDS during '
--                    'negotiation, falling back to OVF'
--                )
--                pubkey_info = self.cfg.get('_pubkeys', None)
--
--            metadata_func = partial(get_metadata_from_fabric,
--                                    fallback_lease_file=self.
--                                    dhclient_lease_file,
--                                    pubkey_info=pubkey_info)
--        else:
--            metadata_func = self.get_metadata_from_agent
-+        pubkey_info = None
-+        try:
-+            raise KeyError(
-+                "Not using public SSH keys from IMDS"
-+            )
-+            # pylint:disable=unreachable
-+            public_keys = self.metadata['imds']['compute']['publicKeys']
-+            LOG.debug(
-+                'Successfully retrieved %s key(s) from IMDS',
-+                len(public_keys)
-+                if public_keys is not None
-+                else 0
-+            )
-+        except KeyError:
-+            LOG.debug(
-+                'Unable to retrieve SSH keys from IMDS during '
-+                'negotiation, falling back to OVF'
-+            )
-+            pubkey_info = self.cfg.get('_pubkeys', None)
-+
-+        metadata_func = partial(get_metadata_from_fabric,
-+                                fallback_lease_file=self.
-+                                dhclient_lease_file,
-+                                pubkey_info=pubkey_info)
- 
-         LOG.debug("negotiating with fabric via agent command %s",
-                   self.ds_cfg['agent_command'])
-@@ -1617,33 +1567,6 @@ def perform_hostname_bounce(hostname, cfg, prev_hostname):
-     return True
- 
- 
--@azure_ds_telemetry_reporter
--def crtfile_to_pubkey(fname, data=None):
--    pipeline = ('openssl x509 -noout -pubkey < "$0" |'
--                'ssh-keygen -i -m PKCS8 -f /dev/stdin')
--    (out, _err) = subp.subp(['sh', '-c', pipeline, fname],
--                            capture=True, data=data)
--    return out.rstrip()
--
--
--@azure_ds_telemetry_reporter
--def pubkeys_from_crt_files(flist):
--    pubkeys = []
--    errors = []
--    for fname in flist:
--        try:
--            pubkeys.append(crtfile_to_pubkey(fname))
--        except subp.ProcessExecutionError:
--            errors.append(fname)
--
--    if errors:
--        report_diagnostic_event(
--            "failed to convert the crt files to pubkey: %s" % errors,
--            logger_func=LOG.warning)
--
--    return pubkeys
--
--
- @azure_ds_telemetry_reporter
- def write_files(datadir, files, dirmode=None):
- 
-@@ -1672,16 +1595,6 @@ def write_files(datadir, files, dirmode=None):
-         util.write_file(filename=fname, content=content, mode=0o600)
- 
- 
--@azure_ds_telemetry_reporter
--def invoke_agent(cmd):
--    # this is a function itself to simplify patching it for test
--    if cmd:
--        LOG.debug("invoking agent: %s", cmd)
--        subp.subp(cmd, shell=(not isinstance(cmd, list)))
--    else:
--        LOG.debug("not invoking agent")
--
--
- def find_child(node, filter_func):
-     ret = []
-     if not node.hasChildNodes():
-diff --git a/doc/rtd/topics/datasources/azure.rst b/doc/rtd/topics/datasources/azure.rst
-index e04c3a33..ad9f2236 100644
---- a/doc/rtd/topics/datasources/azure.rst
-+++ b/doc/rtd/topics/datasources/azure.rst
-@@ -5,28 +5,6 @@ Azure
- 
- This datasource finds metadata and user-data from the Azure cloud platform.
- 
--walinuxagent
--------------
--walinuxagent has several functions within images.  For cloud-init
--specifically, the relevant functionality it performs is to register the
--instance with the Azure cloud platform at boot so networking will be
--permitted.  For more information about the other functionality of
--walinuxagent, see `Azure's documentation
--<https://github.com/Azure/WALinuxAgent#introduction>`_ for more details.
--(Note, however, that only one of walinuxagent's provisioning and cloud-init
--should be used to perform instance customisation.)
--
--If you are configuring walinuxagent yourself, you will want to ensure that you
--have `Provisioning.UseCloudInit
--<https://github.com/Azure/WALinuxAgent#provisioningusecloudinit>`_ set to
--``y``.
--
--
--Builtin Agent
---------------
--An alternative to using walinuxagent to register to the Azure cloud platform
--is to use the ``__builtin__`` agent command.  This section contains more
--background on what that code path does, and how to enable it.
- 
- The Azure cloud platform provides initial data to an instance via an attached
- CD formatted in UDF.  That CD contains a 'ovf-env.xml' file that provides some
-@@ -41,16 +19,6 @@ by calling a script in /etc/dhcp/dhclient-exit-hooks or a file in
- 'dhclient_hook' of cloud-init itself. This sub-command will write the client
- information in json format to /run/cloud-init/dhclient.hook/<interface>.json.
- 
--In order for cloud-init to leverage this method to find the endpoint, the
--cloud.cfg file must contain:
--
--.. sourcecode:: yaml
--
--  datasource:
--    Azure:
--      set_hostname: False
--      agent_command: __builtin__
--
- If those files are not available, the fallback is to check the leases file
- for the endpoint server (again option 245).
- 
-@@ -83,9 +51,6 @@ configuration (in ``/etc/cloud/cloud.cfg`` or ``/etc/cloud/cloud.cfg.d/``).
- 
- The settings that may be configured are:
- 
-- * **agent_command**: Either __builtin__ (default) or a command to run to getcw
--   metadata. If __builtin__, get metadata from walinuxagent. Otherwise run the
--   provided command to obtain metadata.
-  * **apply_network_config**: Boolean set to True to use network configuration
-    described by Azure's IMDS endpoint instead of fallback network config of
-    dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is
-@@ -121,7 +86,6 @@ An example configuration with the default values is provided below:
- 
-   datasource:
-     Azure:
--      agent_command: __builtin__
-       apply_network_config: true
-       data_dir: /var/lib/waagent
-       dhclient_lease_file: /var/lib/dhcp/dhclient.eth0.leases
-@@ -144,9 +108,7 @@ child of the ``LinuxProvisioningConfigurationSet`` (a sibling to ``UserName``)
- If both ``UserData`` and ``CustomData`` are provided behavior is undefined on
- which will be selected.
- 
--In the example below, user-data provided is 'this is my userdata', and the
--datasource config provided is ``{"agent_command": ["start", "walinuxagent"]}``.
--That agent command will take affect as if it were specified in system config.
-+In the example below, user-data provided is 'this is my userdata'
- 
- Example:
- 
-@@ -184,20 +146,16 @@ The hostname is provided to the instance in the ovf-env.xml file as
- Whatever value the instance provides in its dhcp request will resolve in the
- domain returned in the 'search' request.
- 
--The interesting issue is that a generic image will already have a hostname
--configured.  The ubuntu cloud images have 'ubuntu' as the hostname of the
--system, and the initial dhcp request on eth0 is not guaranteed to occur after
--the datasource code has been run.  So, on first boot, that initial value will
--be sent in the dhcp request and *that* value will resolve.
--
--In order to make the ``HostName`` provided in the ovf-env.xml resolve, a
--dhcp request must be made with the new value.  Walinuxagent (in its current
--version) handles this by polling the state of hostname and bouncing ('``ifdown
--eth0; ifup eth0``' the network interface if it sees that a change has been
--made.
-+A generic image will already have a hostname configured.  The ubuntu
-+cloud images have 'ubuntu' as the hostname of the system, and the
-+initial dhcp request on eth0 is not guaranteed to occur after the
-+datasource code has been run.  So, on first boot, that initial value
-+will be sent in the dhcp request and *that* value will resolve.
- 
--cloud-init handles this by setting the hostname in the DataSource's 'get_data'
--method via '``hostname $HostName``', and then bouncing the interface.  This
-+In order to make the ``HostName`` provided in the ovf-env.xml resolve,
-+a dhcp request must be made with the new value. cloud-init handles
-+this by setting the hostname in the DataSource's 'get_data' method via
-+'``hostname $HostName``', and then bouncing the interface.  This
- behavior can be configured or disabled in the datasource config.  See
- 'Configuration' above.
- 
-diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
-index dedebeb1..320fa857 100644
---- a/tests/unittests/test_datasource/test_azure.py
-+++ b/tests/unittests/test_datasource/test_azure.py
-@@ -638,17 +638,10 @@ scbus-1 on xpt0 bus 0
-         def dsdevs():
-             return data.get('dsdevs', [])
- 
--        def _invoke_agent(cmd):
--            data['agent_invoked'] = cmd
--
-         def _wait_for_files(flist, _maxwait=None, _naplen=None):
-             data['waited'] = flist
-             return []
- 
--        def _pubkeys_from_crt_files(flist):
--            data['pubkey_files'] = flist
--            return ["pubkey_from: %s" % f for f in flist]
--
-         if data.get('ovfcontent') is not None:
-             populate_dir(os.path.join(self.paths.seed_dir, "azure"),
-                          {'ovf-env.xml': data['ovfcontent']})
-@@ -675,8 +668,6 @@ scbus-1 on xpt0 bus 0
- 
-         self.apply_patches([
-             (dsaz, 'list_possible_azure_ds_devs', dsdevs),
--            (dsaz, 'invoke_agent', _invoke_agent),
--            (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),
-             (dsaz, 'perform_hostname_bounce', mock.MagicMock()),
-             (dsaz, 'get_hostname', mock.MagicMock()),
-             (dsaz, 'set_hostname', mock.MagicMock()),
-@@ -765,7 +756,6 @@ scbus-1 on xpt0 bus 0
-             ret = dsrc.get_data()
-             self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
-             self.assertFalse(ret)
--            self.assertNotIn('agent_invoked', data)
-             # Assert that for non viable platforms,
-             # there is no communication with the Azure datasource.
-             self.assertEqual(
-@@ -789,7 +779,6 @@ scbus-1 on xpt0 bus 0
-             ret = dsrc.get_data()
-             self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
-             self.assertFalse(ret)
--            self.assertNotIn('agent_invoked', data)
-             self.assertEqual(
-                 1,
-                 m_report_failure.call_count)
-@@ -806,7 +795,6 @@ scbus-1 on xpt0 bus 0
-                 1,
-                 m_crawl_metadata.call_count)
-             self.assertFalse(ret)
--            self.assertNotIn('agent_invoked', data)
- 
-     def test_crawl_metadata_exception_should_report_failure_with_msg(self):
-         data = {}
-@@ -1086,21 +1074,6 @@ scbus-1 on xpt0 bus 0
-         self.assertTrue(os.path.isdir(self.waagent_d))
-         self.assertEqual(stat.S_IMODE(os.stat(self.waagent_d).st_mode), 0o700)
- 
--    def test_user_cfg_set_agent_command_plain(self):
--        # set dscfg in via plaintext
--        # we must have friendly-to-xml formatted plaintext in yaml_cfg
--        # not all plaintext is expected to work.
--        yaml_cfg = "{agent_command: my_command}\n"
--        cfg = yaml.safe_load(yaml_cfg)
--        odata = {'HostName': "myhost", 'UserName': "myuser",
--                 'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}}
--        data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
--
--        dsrc = self._get_ds(data)
--        ret = self._get_and_setup(dsrc)
--        self.assertTrue(ret)
--        self.assertEqual(data['agent_invoked'], cfg['agent_command'])
--
-     @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
-                 return_value=None)
-     def test_network_config_set_from_imds(self, m_driver):
-@@ -1205,29 +1178,6 @@ scbus-1 on xpt0 bus 0
-         dsrc.get_data()
-         self.assertEqual('eastus2', dsrc.region)
- 
--    def test_user_cfg_set_agent_command(self):
--        # set dscfg in via base64 encoded yaml
--        cfg = {'agent_command': "my_command"}
--        odata = {'HostName': "myhost", 'UserName': "myuser",
--                 'dscfg': {'text': b64e(yaml.dump(cfg)),
--                           'encoding': 'base64'}}
--        data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
--
--        dsrc = self._get_ds(data)
--        ret = self._get_and_setup(dsrc)
--        self.assertTrue(ret)
--        self.assertEqual(data['agent_invoked'], cfg['agent_command'])
--
--    def test_sys_cfg_set_agent_command(self):
--        sys_cfg = {'datasource': {'Azure': {'agent_command': '_COMMAND'}}}
--        data = {'ovfcontent': construct_valid_ovf_env(data={}),
--                'sys_cfg': sys_cfg}
--
--        dsrc = self._get_ds(data)
--        ret = self._get_and_setup(dsrc)
--        self.assertTrue(ret)
--        self.assertEqual(data['agent_invoked'], '_COMMAND')
--
-     def test_sys_cfg_set_never_destroy_ntfs(self):
-         sys_cfg = {'datasource': {'Azure': {
-             'never_destroy_ntfs': 'user-supplied-value'}}}
-@@ -1311,51 +1261,6 @@ scbus-1 on xpt0 bus 0
-         self.assertTrue(ret)
-         self.assertEqual(dsrc.userdata_raw, mydata.encode('utf-8'))
- 
--    def test_cfg_has_pubkeys_fingerprint(self):
--        odata = {'HostName': "myhost", 'UserName': "myuser"}
--        mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': ''}]
--        pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist]
--        data = {'ovfcontent': construct_valid_ovf_env(data=odata,
--                                                      pubkeys=pubkeys)}
--
--        dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
--        ret = self._get_and_setup(dsrc)
--        self.assertTrue(ret)
--        for mypk in mypklist:
--            self.assertIn(mypk, dsrc.cfg['_pubkeys'])
--            self.assertIn('pubkey_from', dsrc.metadata['public-keys'][-1])
--
--    def test_cfg_has_pubkeys_value(self):
--        # make sure that provided key is used over fingerprint
--        odata = {'HostName': "myhost", 'UserName': "myuser"}
--        mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': 'value1'}]
--        pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist]
--        data = {'ovfcontent': construct_valid_ovf_env(data=odata,
--                                                      pubkeys=pubkeys)}
--
--        dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
--        ret = self._get_and_setup(dsrc)
--        self.assertTrue(ret)
--
--        for mypk in mypklist:
--            self.assertIn(mypk, dsrc.cfg['_pubkeys'])
--            self.assertIn(mypk['value'], dsrc.metadata['public-keys'])
--
--    def test_cfg_has_no_fingerprint_has_value(self):
--        # test value is used when fingerprint not provided
--        odata = {'HostName': "myhost", 'UserName': "myuser"}
--        mypklist = [{'fingerprint': None, 'path': 'path1', 'value': 'value1'}]
--        pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist]
--        data = {'ovfcontent': construct_valid_ovf_env(data=odata,
--                                                      pubkeys=pubkeys)}
--
--        dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
--        ret = self._get_and_setup(dsrc)
--        self.assertTrue(ret)
--
--        for mypk in mypklist:
--            self.assertIn(mypk['value'], dsrc.metadata['public-keys'])
--
-     def test_default_ephemeral_configs_ephemeral_exists(self):
-         # make sure the ephemeral configs are correct if disk present
-         odata = {}
-@@ -1919,8 +1824,6 @@ class TestAzureBounce(CiTestCase):
-     with_logs = True
- 
-     def mock_out_azure_moving_parts(self):
--        self.patches.enter_context(
--            mock.patch.object(dsaz, 'invoke_agent'))
-         self.patches.enter_context(
-             mock.patch.object(dsaz.util, 'wait_for_files'))
-         self.patches.enter_context(
--- 
-2.27.0
-
diff --git a/SOURCES/ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch b/SOURCES/ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch
deleted file mode 100644
index 44ad400..0000000
--- a/SOURCES/ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch
+++ /dev/null
@@ -1,97 +0,0 @@
-From 478709d7c157a085e3b2fee432e24978a3485234 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Wed, 20 Oct 2021 16:28:42 +0200
-Subject: [PATCH] cc_ssh.py: fix private key group owner and permissions
- (#1070)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 32: cc_ssh.py: fix private key group owner and permissions (#1070)
-RH-Commit: [1/1] 0382c3f671ae0fa9cab23dfad1f636967b012148
-RH-Bugzilla: 2013644
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-commit ee296ced9c0a61b1484d850b807c601bcd670ec1
-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date:   Tue Oct 19 21:32:10 2021 +0200
-
-    cc_ssh.py: fix private key group owner and permissions (#1070)
-
-    When default host keys are created by sshd-keygen (/etc/ssh/ssh_host_*_key)
-    in RHEL/CentOS/Fedora, openssh it performs the following:
-
-    # create new keys
-    if ! $KEYGEN -q -t $KEYTYPE -f $KEY -C '' -N '' >&/dev/null; then
-            exit 1
-    fi
-
-    # sanitize permissions
-    /usr/bin/chgrp ssh_keys $KEY
-    /usr/bin/chmod 640 $KEY
-    /usr/bin/chmod 644 $KEY.pub
-    Note that the group ssh_keys exists only in RHEL/CentOS/Fedora.
-
-    Now that we disable sshd-keygen to allow only cloud-init to create
-    them, we miss the "sanitize permissions" part, where we set the group
-    owner as ssh_keys and the private key mode to 640.
-
-    According to https://bugzilla.redhat.com/show_bug.cgi?id=2013644#c8, failing
-    to set group ownership and permissions like openssh does makes the RHEL openscap
-    tool generate an error.
-
-    Signed-off-by: Emanuele Giuseppe Esposito eesposit@redhat.com
-
-    RHBZ: 2013644
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/config/cc_ssh.py |  7 +++++++
- cloudinit/util.py          | 14 ++++++++++++++
- 2 files changed, 21 insertions(+)
-
-diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
-index 05a16dbc..4e986c55 100755
---- a/cloudinit/config/cc_ssh.py
-+++ b/cloudinit/config/cc_ssh.py
-@@ -240,6 +240,13 @@ def handle(_name, cfg, cloud, log, _args):
-                 try:
-                     out, err = subp.subp(cmd, capture=True, env=lang_c)
-                     sys.stdout.write(util.decode_binary(out))
-+
-+                    gid = util.get_group_id("ssh_keys")
-+                    if gid != -1:
-+                        # perform same "sanitize permissions" as sshd-keygen
-+                        os.chown(keyfile, -1, gid)
-+                        os.chmod(keyfile, 0o640)
-+                        os.chmod(keyfile + ".pub", 0o644)
-                 except subp.ProcessExecutionError as e:
-                     err = util.decode_binary(e.stderr).lower()
-                     if (e.exit_code == 1 and
-diff --git a/cloudinit/util.py b/cloudinit/util.py
-index 343976ad..fe37ae89 100644
---- a/cloudinit/util.py
-+++ b/cloudinit/util.py
-@@ -1831,6 +1831,20 @@ def chmod(path, mode):
-             os.chmod(path, real_mode)
- 
- 
-+def get_group_id(grp_name: str) -> int:
-+    """
-+    Returns the group id of a group name, or -1 if no group exists
-+
-+    @param grp_name: the name of the group
-+    """
-+    gid = -1
-+    try:
-+        gid = grp.getgrnam(grp_name).gr_gid
-+    except KeyError:
-+        LOG.debug("Group %s is not a valid group name", grp_name)
-+    return gid
-+
-+
- def get_permissions(path: str) -> int:
-     """
-     Returns the octal permissions of the file/folder pointed by the path,
--- 
-2.27.0
-
diff --git a/SOURCES/ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch b/SOURCES/ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch
deleted file mode 100644
index 9ea95c1..0000000
--- a/SOURCES/ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch
+++ /dev/null
@@ -1,87 +0,0 @@
-From ea83e72b335e652b080fda66a075c0d1322ed6dc Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Tue, 7 Dec 2021 10:00:41 +0100
-Subject: [PATCH] cloudinit/net: handle two different routes for the same ip
- (#1124)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 39: cloudinit/net: handle two different routes for the same ip (#1124)
-RH-Commit: [1/1] 6810dc29ce786fbca96d2033386aa69c6ab65997
-RH-Bugzilla: 2028028
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-
-commit 0e25076b34fa995161b83996e866c0974cee431f
-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date:   Mon Dec 6 18:34:26 2021 +0100
-
-    cloudinit/net: handle two different routes for the same ip (#1124)
-
-    If we set a dhcp server side like this:
-    $ cat /var/tmp/cloud-init/cloud-init-dhcp-f0rie5tm/dhcp.leases
-    lease {
-    ...
-    option classless-static-routes 31.169.254.169.254 0.0.0.0,31.169.254.169.254
-        10.112.143.127,22.10.112.140 0.0.0.0,0 10.112.140.1;
-    ...
-    }
-    cloud-init fails to configure the routes via 'ip route add' because to there are
-    two different routes for 169.254.169.254:
-
-    $ ip -4 route add 192.168.1.1/32 via 0.0.0.0 dev eth0
-    $ ip -4 route add 192.168.1.1/32 via 10.112.140.248 dev eth0
-
-    But NetworkManager can handle such scenario successfully as it uses "ip route append".
-    So change cloud-init to also use "ip route append" to fix the issue:
-
-    $ ip -4 route append 192.168.1.1/32 via 0.0.0.0 dev eth0
-    $ ip -4 route append 192.168.1.1/32 via 10.112.140.248 dev eth0
-
-    Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-
-    RHBZ: #2003231
-
-Conflicts:
-    cloudinit/net/tests/test_init.py: a mock call in
-    test_ephemeral_ipv4_network_with_rfc3442_static_routes is not
-    present downstream.
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/net/__init__.py        | 2 +-
- cloudinit/net/tests/test_init.py | 4 ++--
- 2 files changed, 3 insertions(+), 3 deletions(-)
-
-diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
-index 385b7bcc..003efa2a 100644
---- a/cloudinit/net/__init__.py
-+++ b/cloudinit/net/__init__.py
-@@ -1138,7 +1138,7 @@ class EphemeralIPv4Network(object):
-             if gateway != "0.0.0.0/0":
-                 via_arg = ['via', gateway]
-             subp.subp(
--                ['ip', '-4', 'route', 'add', net_address] + via_arg +
-+                ['ip', '-4', 'route', 'append', net_address] + via_arg +
-                 ['dev', self.interface], capture=True)
-             self.cleanup_cmds.insert(
-                 0, ['ip', '-4', 'route', 'del', net_address] + via_arg +
-diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
-index 946f8ee2..2350837b 100644
---- a/cloudinit/net/tests/test_init.py
-+++ b/cloudinit/net/tests/test_init.py
-@@ -719,10 +719,10 @@ class TestEphemeralIPV4Network(CiTestCase):
-                 ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
-                 capture=True),
-             mock.call(
--                ['ip', '-4', 'route', 'add', '169.254.169.254/32',
-+                ['ip', '-4', 'route', 'append', '169.254.169.254/32',
-                  'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
-             mock.call(
--                ['ip', '-4', 'route', 'add', '0.0.0.0/0',
-+                ['ip', '-4', 'route', 'append', '0.0.0.0/0',
-                  'via', '192.168.2.1', 'dev', 'eth0'], capture=True)]
-         expected_teardown_calls = [
-             mock.call(
--- 
-2.27.0
-
diff --git a/SOURCES/ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch b/SOURCES/ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch
deleted file mode 100644
index f257a67..0000000
--- a/SOURCES/ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch
+++ /dev/null
@@ -1,173 +0,0 @@
-From 005d0a98c69d154a00e9fd599c7fbe5aef73c933 Mon Sep 17 00:00:00 2001
-From: Amy Chen <xiachen@redhat.com>
-Date: Thu, 25 Nov 2021 18:30:48 +0800
-Subject: [PATCH] fix error on upgrade caused by new vendordata2 attributes
-
-RH-Author: xiachen <None>
-RH-MergeRequest: 35: fix error on upgrade caused by new vendordata2 attributes
-RH-Commit: [1/1] 9e00a7744838afbbdc5eb14628b7f572beba9f19
-RH-Bugzilla: 2021538
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-
-commit d132356cc361abef2d90d4073438f3ab759d5964
-Author: James Falcon <TheRealFalcon@users.noreply.github.com>
-Date:   Mon Apr 19 11:31:28 2021 -0500
-
-    fix error on upgrade caused by new vendordata2 attributes (#869)
-
-    In #777, we added 'vendordata2' and 'vendordata2_raw' attributes to
-    the DataSource class, but didn't use the upgrade framework to deal
-    with an unpickle after upgrade. This commit adds the necessary
-    upgrade code.
-
-    Additionally, added a smaller-scope upgrade test to our integration
-    tests that will be run on every CI run so we catch these issues
-    immediately in the future.
-
-    LP: #1922739
-
-Signed-off-by: Amy Chen <xiachen@redhat.com>
----
- cloudinit/sources/__init__.py           | 12 +++++++++++-
- cloudinit/tests/test_upgrade.py         |  4 ++++
- tests/integration_tests/clouds.py       |  4 ++--
- tests/integration_tests/test_upgrade.py | 25 ++++++++++++++++++++++++-
- 4 files changed, 41 insertions(+), 4 deletions(-)
-
-diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
-index 1ad1880d..7d74f8d9 100644
---- a/cloudinit/sources/__init__.py
-+++ b/cloudinit/sources/__init__.py
-@@ -24,6 +24,7 @@ from cloudinit import util
- from cloudinit.atomic_helper import write_json
- from cloudinit.event import EventType
- from cloudinit.filters import launch_index
-+from cloudinit.persistence import CloudInitPickleMixin
- from cloudinit.reporting import events
- 
- DSMODE_DISABLED = "disabled"
-@@ -134,7 +135,7 @@ URLParams = namedtuple(
-     'URLParms', ['max_wait_seconds', 'timeout_seconds', 'num_retries'])
- 
- 
--class DataSource(metaclass=abc.ABCMeta):
-+class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta):
- 
-     dsmode = DSMODE_NETWORK
-     default_locale = 'en_US.UTF-8'
-@@ -196,6 +197,8 @@ class DataSource(metaclass=abc.ABCMeta):
-     # non-root users
-     sensitive_metadata_keys = ('merged_cfg', 'security-credentials',)
- 
-+    _ci_pkl_version = 1
-+
-     def __init__(self, sys_cfg, distro, paths, ud_proc=None):
-         self.sys_cfg = sys_cfg
-         self.distro = distro
-@@ -218,6 +221,13 @@ class DataSource(metaclass=abc.ABCMeta):
-         else:
-             self.ud_proc = ud_proc
- 
-+    def _unpickle(self, ci_pkl_version: int) -> None:
-+        """Perform deserialization fixes for Paths."""
-+        if not hasattr(self, 'vendordata2'):
-+            self.vendordata2 = None
-+        if not hasattr(self, 'vendordata2_raw'):
-+            self.vendordata2_raw = None
-+
-     def __str__(self):
-         return type_utils.obj_name(self)
- 
-diff --git a/cloudinit/tests/test_upgrade.py b/cloudinit/tests/test_upgrade.py
-index f79a2536..fd3c5812 100644
---- a/cloudinit/tests/test_upgrade.py
-+++ b/cloudinit/tests/test_upgrade.py
-@@ -43,3 +43,7 @@ class TestUpgrade:
-     def test_blacklist_drivers_set_on_networking(self, previous_obj_pkl):
-         """We always expect Networking.blacklist_drivers to be initialised."""
-         assert previous_obj_pkl.distro.networking.blacklist_drivers is None
-+
-+    def test_vendordata_exists(self, previous_obj_pkl):
-+        assert previous_obj_pkl.vendordata2 is None
-+        assert previous_obj_pkl.vendordata2_raw is None
-diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py
-index 9527a413..1d0b9d83 100644
---- a/tests/integration_tests/clouds.py
-+++ b/tests/integration_tests/clouds.py
-@@ -100,14 +100,14 @@ class IntegrationCloud(ABC):
-             # Even if we're using the default key, it may still have a
-             # different name in the clouds, so we need to set it separately.
-             self.cloud_instance.key_pair.name = settings.KEYPAIR_NAME
--        self._released_image_id = self._get_initial_image()
-+        self.released_image_id = self._get_initial_image()
-         self.snapshot_id = None
- 
-     @property
-     def image_id(self):
-         if self.snapshot_id:
-             return self.snapshot_id
--        return self._released_image_id
-+        return self.released_image_id
- 
-     def emit_settings_to_log(self) -> None:
-         log.info(
-diff --git a/tests/integration_tests/test_upgrade.py b/tests/integration_tests/test_upgrade.py
-index c20cb3c1..48e0691b 100644
---- a/tests/integration_tests/test_upgrade.py
-+++ b/tests/integration_tests/test_upgrade.py
-@@ -1,4 +1,5 @@
- import logging
-+import os
- import pytest
- import time
- from pathlib import Path
-@@ -8,6 +9,8 @@ from tests.integration_tests.conftest import (
-     get_validated_source,
-     session_start_time,
- )
-+from tests.integration_tests.instances import CloudInitSource
-+
- 
- log = logging.getLogger('integration_testing')
- 
-@@ -63,7 +66,7 @@ def test_upgrade(session_cloud: IntegrationCloud):
-         return  # type checking doesn't understand that skip raises
- 
-     launch_kwargs = {
--        'image_id': session_cloud._get_initial_image(),
-+        'image_id': session_cloud.released_image_id,
-     }
- 
-     image = ImageSpecification.from_os_image()
-@@ -93,6 +96,26 @@ def test_upgrade(session_cloud: IntegrationCloud):
-         instance.install_new_cloud_init(source, take_snapshot=False)
-         instance.execute('hostname something-else')
-         _restart(instance)
-+        assert instance.execute('cloud-init status --wait --long').ok
-         _output_to_compare(instance, after_path, netcfg_path)
- 
-     log.info('Wrote upgrade test logs to %s and %s', before_path, after_path)
-+
-+
-+@pytest.mark.ci
-+@pytest.mark.ubuntu
-+def test_upgrade_package(session_cloud: IntegrationCloud):
-+    if get_validated_source(session_cloud) != CloudInitSource.DEB_PACKAGE:
-+        not_run_message = 'Test only supports upgrading to build deb'
-+        if os.environ.get('TRAVIS'):
-+            # If this isn't running on CI, we should know
-+            pytest.fail(not_run_message)
-+        else:
-+            pytest.skip(not_run_message)
-+
-+    launch_kwargs = {'image_id': session_cloud.released_image_id}
-+
-+    with session_cloud.launch(launch_kwargs=launch_kwargs) as instance:
-+        instance.install_deb()
-+        instance.restart()
-+        assert instance.execute('cloud-init status --wait --long').ok
--- 
-2.27.0
-
diff --git a/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch b/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
deleted file mode 100644
index be1e283..0000000
--- a/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
+++ /dev/null
@@ -1,65 +0,0 @@
-From abf1adeae8211f5acd87dc63b03b2ed995047efd Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Thu, 20 May 2021 08:53:55 +0200
-Subject: [PATCH 1/2] rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and
- set in cloud.cfg
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 10: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in cloud.cfg
-RH-Commit: [1/1] 6da989423b9b6e017afbac2f1af3649b0487310f
-RH-Bugzilla: 1957532
-RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
-RH-Acked-by: Cathy Avery <cavery@redhat.com>
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-Currently genkeytypes in cloud.cfg is set to None, so together with
-ssh_deletekeys=1 cloudinit on first boot it will just delete the existing
-keys and not generate new ones.
-
-Just removing that property in cloud.cfg is not enough, because
-settings.py provides another empty default value that will be used
-instead, resulting to no key generated even when the property is not defined.
-
-Removing genkeytypes also in settings.py will default to GENERATE_KEY_NAMES,
-but since we want only 'rsa', 'ecdsa' and 'ed25519', add back genkeytypes in
-cloud.cfg with the above defaults.
-
-Also remove ssh_deletekeys in settings.py as we always need
-to 1 (and it also defaults to 1).
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/settings.py | 2 --
- rhel/cloud.cfg        | 2 +-
- 2 files changed, 1 insertion(+), 3 deletions(-)
-
-diff --git a/cloudinit/settings.py b/cloudinit/settings.py
-index 43a1490c..2acf2615 100644
---- a/cloudinit/settings.py
-+++ b/cloudinit/settings.py
-@@ -49,8 +49,6 @@ CFG_BUILTIN = {
-     'def_log_file_mode': 0o600,
-     'log_cfgs': [],
-     'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'],
--    'ssh_deletekeys': False,
--    'ssh_genkeytypes': [],
-     'syslog_fix_perms': [],
-     'system_info': {
-         'paths': {
-diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
-index 9ecba215..cbee197a 100644
---- a/rhel/cloud.cfg
-+++ b/rhel/cloud.cfg
-@@ -7,7 +7,7 @@ ssh_pwauth:   0
- mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
- resize_rootfs_tmp: /dev
- ssh_deletekeys:   1
--ssh_genkeytypes:  ~
-+ssh_genkeytypes:  ['rsa', 'ecdsa', 'ed25519']
- syslog_fix_perms: ~
- disable_vmware_customization: false
- 
--- 
-2.27.0
-
diff --git a/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch
deleted file mode 100644
index bdec823..0000000
--- a/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch
+++ /dev/null
@@ -1,653 +0,0 @@
-From aeab67600eb2d5e483812620b56ce5fb031a57d6 Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Mon, 12 Jul 2021 21:47:37 +0200
-Subject: [PATCH] ssh-util: allow cloudinit to merge all ssh keys into a custom
- user file, defined in AuthorizedKeysFile (#937)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 25: ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
-RH-Commit: [1/1] 27bbe94f3b9dd8734865766bd30b06cff83383ab (eesposit/cloud-init)
-RH-Bugzilla: 1862967
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-TESTED: By me and QA
-BREW: 38030830
-
-Conflicts: upstream patch modifies tests/integration_tests/util.py, that is
-not present in RHEL.
-
-commit 9b52405c6f0de5e00d5ee9c1d13540425d8f6bf5
-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date:   Mon Jul 12 20:21:02 2021 +0200
-
-    ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
-
-    This patch aims to fix LP1911680, by analyzing the files provided
-    in sshd_config and merge all keys into an user-specific file. Also
-    introduces additional tests to cover this specific case.
-
-    The file is picked by analyzing the path given in AuthorizedKeysFile.
-
-    If it points inside the current user folder (path is /home/user/*), it
-    means it is an user-specific file, so we can copy all user-keys there.
-    If it contains a %u or %h, it means that there will be a specific
-    authorized_keys file for each user, so we can copy all user-keys there.
-    If no path points to an user-specific file, for example when only
-    /etc/ssh/authorized_keys is given, default to ~/.ssh/authorized_keys.
-    Note that if there are more than a single user-specific file, the last
-    one will be picked.
-
-    Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-    Co-authored-by: James Falcon <therealfalcon@gmail.com>
-
-    LP: #1911680
-    RHBZ:1862967
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/ssh_util.py                         |  22 +-
- .../assets/keys/id_rsa.test1                  |  38 +++
- .../assets/keys/id_rsa.test1.pub              |   1 +
- .../assets/keys/id_rsa.test2                  |  38 +++
- .../assets/keys/id_rsa.test2.pub              |   1 +
- .../assets/keys/id_rsa.test3                  |  38 +++
- .../assets/keys/id_rsa.test3.pub              |   1 +
- .../modules/test_ssh_keysfile.py              |  85 ++++++
- tests/unittests/test_sshutil.py               | 246 +++++++++++++++++-
- 9 files changed, 456 insertions(+), 14 deletions(-)
- create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1
- create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1.pub
- create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2
- create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2.pub
- create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3
- create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3.pub
- create mode 100644 tests/integration_tests/modules/test_ssh_keysfile.py
-
-diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
-index c08042d6..89057262 100644
---- a/cloudinit/ssh_util.py
-+++ b/cloudinit/ssh_util.py
-@@ -252,13 +252,15 @@ def render_authorizedkeysfile_paths(value, homedir, username):
- def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
-     (ssh_dir, pw_ent) = users_ssh_info(username)
-     default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys')
-+    user_authorizedkeys_file = default_authorizedkeys_file
-     auth_key_fns = []
-     with util.SeLinuxGuard(ssh_dir, recursive=True):
-         try:
-             ssh_cfg = parse_ssh_config_map(sshd_cfg_file)
-+            key_paths = ssh_cfg.get("authorizedkeysfile",
-+                                    "%h/.ssh/authorized_keys")
-             auth_key_fns = render_authorizedkeysfile_paths(
--                ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"),
--                pw_ent.pw_dir, username)
-+                key_paths, pw_ent.pw_dir, username)
- 
-         except (IOError, OSError):
-             # Give up and use a default key filename
-@@ -267,8 +269,22 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
-                         "config from %r, using 'AuthorizedKeysFile' file "
-                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
- 
-+    # check if one of the keys is the user's one
-+    for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns):
-+        if any([
-+            '%u' in key_path,
-+            '%h' in key_path,
-+            auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir))
-+        ]):
-+            user_authorizedkeys_file = auth_key_fn
-+
-+    if user_authorizedkeys_file != default_authorizedkeys_file:
-+        LOG.debug(
-+            "AuthorizedKeysFile has an user-specific authorized_keys, "
-+            "using %s", user_authorizedkeys_file)
-+
-     # always store all the keys in the user's private file
--    return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
-+    return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
- 
- 
- def setup_user_keys(keys, username, options=None):
-diff --git a/tests/integration_tests/assets/keys/id_rsa.test1 b/tests/integration_tests/assets/keys/id_rsa.test1
-new file mode 100644
-index 00000000..bd4c822e
---- /dev/null
-+++ b/tests/integration_tests/assets/keys/id_rsa.test1
-@@ -0,0 +1,38 @@
-+-----BEGIN OPENSSH PRIVATE KEY-----
-+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
-+NhAAAAAwEAAQAAAYEAtRlG96aJ23URvAgO/bBsuLl+lquc350aSwV98/i8vlvOn5GVcHye
-+t/rXQg4lZ4s0owG3kWyQFY8nvTk+G+UNU8fN0anAzBDi+4MzsejkF9scjTMFmXVrIpICqV
-+3bYQNjPv6r+ubQdkD01du3eB9t5/zl84gtshp0hBdofyz8u1/A25s7fVU67GyI7PdKvaS+
-+yvJSInZnb2e9VQzfJC+qAnN7gUZatBKjdgUtJeiUUeDaVnaS17b0aoT9iBO0sIcQtOTBlY
-+lCjFt1TAMLZ64Hj3SfGZB7Yj0Z+LzFB2IWX1zzsjI68YkYPKOSL/NYhQU9e55kJQ7WnngN
-+HY/2n/A7dNKSFDmgM5c9IWgeZ7fjpsfIYAoJ/CAxFIND+PEHd1gCS6xoEhaUVyh5WH/Xkw
-+Kv1nx4AiZ2BFCE+75kySRLZUJ+5y0r3DU5ktMXeURzVIP7pu0R8DCul+GU+M/+THyWtAEO
-+geaNJ6fYpo2ipDhbmTYt3kk2lMIapRxGBFs+37sdAAAFgGGJssNhibLDAAAAB3NzaC1yc2
-+EAAAGBALUZRvemidt1EbwIDv2wbLi5fparnN+dGksFffP4vL5bzp+RlXB8nrf610IOJWeL
-+NKMBt5FskBWPJ705PhvlDVPHzdGpwMwQ4vuDM7Ho5BfbHI0zBZl1ayKSAqld22EDYz7+q/
-+rm0HZA9NXbt3gfbef85fOILbIadIQXaH8s/LtfwNubO31VOuxsiOz3Sr2kvsryUiJ2Z29n
-+vVUM3yQvqgJze4FGWrQSo3YFLSXolFHg2lZ2kte29GqE/YgTtLCHELTkwZWJQoxbdUwDC2
-+euB490nxmQe2I9Gfi8xQdiFl9c87IyOvGJGDyjki/zWIUFPXueZCUO1p54DR2P9p/wO3TS
-+khQ5oDOXPSFoHme346bHyGAKCfwgMRSDQ/jxB3dYAkusaBIWlFcoeVh/15MCr9Z8eAImdg
-+RQhPu+ZMkkS2VCfuctK9w1OZLTF3lEc1SD+6btEfAwrpfhlPjP/kx8lrQBDoHmjSen2KaN
-+oqQ4W5k2Ld5JNpTCGqUcRgRbPt+7HQAAAAMBAAEAAAGBAJJCTOd70AC2ptEGbR0EHHqADT
-+Wgefy7A94tHFEqxTy0JscGq/uCGimaY7kMdbcPXT59B4VieWeAC2cuUPP0ZHQSfS5ke7oT
-+tU3N47U+0uBVbNS4rUAH7bOo2o9wptnOA5x/z+O+AARRZ6tEXQOd1oSy4gByLf2Wkh2QTi
-+vP6Hln1vlFgKEzcXg6G8fN3MYWxKRhWmZM3DLERMvorlqqSBLcs5VvfZfLKcsKWTExioAq
-+KgwEjYm8T9+rcpsw1xBus3j9k7wCI1Sus6PCDjq0pcYKLMYM7p8ygnU2tRYrOztdIxgWRA
-+w/1oenm1Mqq2tV5xJcBCwCLOGe6SFwkIRywOYc57j5McH98Xhhg9cViyyBdXy/baF0mro+
-+qPhOsWDxqwD4VKZ9UmQ6O8kPNKcc7QcIpFJhcO0g9zbp/MT0KueaWYrTKs8y4lUkTT7Xz6
-++MzlR122/JwlAbBo6Y2kWtB+y+XwBZ0BfyJsm2czDhKm7OI5KfuBNhq0tFfKwOlYBq4QAA
-+AMAyvUof1R8LLISkdO3EFTKn5RGNkPPoBJmGs6LwvU7NSjjLj/wPQe4jsIBc585tvbrddp
-+60h72HgkZ5tqOfdeBYOKqX0qQQBHUEvI6M+NeQTQRev8bCHMLXQ21vzpClnrwNzlja359E
-+uTRfiPRwIlyPLhOUiClBDSAnBI9h82Hkk3zzsQ/xGfsPB7iOjRbW69bMRSVCRpeweCVmWC
-+77DTsEOq69V2TdljhQNIXE5OcOWonIlfgPiI74cdd+dLhzc/AAAADBAO1/JXd2kYiRyNkZ
-+aXTLcwiSgBQIYbobqVP3OEtTclr0P1JAvby3Y4cCaEhkenx+fBqgXAku5lKM+U1Q9AEsMk
-+cjIhaDpb43rU7GPjMn4zHwgGsEKd5pC1yIQ2PlK+cHanAdsDjIg+6RR+fuvid/mBeBOYXb
-+Py0sa3HyekLJmCdx4UEyNASoiNaGFLQVAqo+RACsXy6VMxFH5dqDYlvwrfUQLwxJmse9Vb
-+GEuuPAsklNugZqssC2XOIujFVUpslduQAAAMEAwzVHQVtsc3icCSzEAARpDTUdTbI29OhB
-+/FMBnjzS9/3SWfLuBOSm9heNCHs2jdGNb8cPdKZuY7S9Fx6KuVUPyTbSSYkjj0F4fTeC9g
-+0ym4p4UWYdF67WSWwLORkaG8K0d+G/CXkz8hvKUg6gcZWKBHAE1ROrHu1nsc8v7mkiKq4I
-+bnTw5Q9TgjbWcQWtgPq0wXyyl/K8S1SFdkMCTOHDD0RQ+jTV2WNGVwFTodIRHenX+Rw2g4
-+CHbTWbsFrHR1qFAAAACmphbWVzQG5ld3Q=
-+-----END OPENSSH PRIVATE KEY-----
-diff --git a/tests/integration_tests/assets/keys/id_rsa.test1.pub b/tests/integration_tests/assets/keys/id_rsa.test1.pub
-new file mode 100644
-index 00000000..3d2e26e1
---- /dev/null
-+++ b/tests/integration_tests/assets/keys/id_rsa.test1.pub
-@@ -0,0 +1 @@
-+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1GUb3ponbdRG8CA79sGy4uX6Wq5zfnRpLBX3z+Ly+W86fkZVwfJ63+tdCDiVnizSjAbeRbJAVjye9OT4b5Q1Tx83RqcDMEOL7gzOx6OQX2xyNMwWZdWsikgKpXdthA2M+/qv65tB2QPTV27d4H23n/OXziC2yGnSEF2h/LPy7X8Dbmzt9VTrsbIjs90q9pL7K8lIidmdvZ71VDN8kL6oCc3uBRlq0EqN2BS0l6JRR4NpWdpLXtvRqhP2IE7SwhxC05MGViUKMW3VMAwtnrgePdJ8ZkHtiPRn4vMUHYhZfXPOyMjrxiRg8o5Iv81iFBT17nmQlDtaeeA0dj/af8Dt00pIUOaAzlz0haB5nt+Omx8hgCgn8IDEUg0P48Qd3WAJLrGgSFpRXKHlYf9eTAq/WfHgCJnYEUIT7vmTJJEtlQn7nLSvcNTmS0xd5RHNUg/um7RHwMK6X4ZT4z/5MfJa0AQ6B5o0np9imjaKkOFuZNi3eSTaUwhqlHEYEWz7fux0= test1@host
-diff --git a/tests/integration_tests/assets/keys/id_rsa.test2 b/tests/integration_tests/assets/keys/id_rsa.test2
-new file mode 100644
-index 00000000..5854d901
---- /dev/null
-+++ b/tests/integration_tests/assets/keys/id_rsa.test2
-@@ -0,0 +1,38 @@
-+-----BEGIN OPENSSH PRIVATE KEY-----
-+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
-+NhAAAAAwEAAQAAAYEAvK50D2PWOc4ikyHVRJS6tDhqzjL5cKiivID4p1X8BYCVw83XAEGO
-+LnItUyVXHNADlh6fpVq1NY6A2JVtygoPF6ZFx8ph7IWMmnhDdnxLLyGsbhd1M1tiXJD/R+
-+3WnGHRJ4PKrQavMLgqHRrieV3QVVfjFSeo6jX/4TruP6ZmvITMZWJrXaGphxJ/pPykEdkO
-+i8AmKU9FNviojyPS2nNtj9B/635IdgWvrd7Vf5Ycsw9MR55LWSidwa856RH62Yl6LpEGTH
-+m1lJiMk1u88JPSqvohhaUkLKkFpcQwcB0m76W1KOyllJsmX8bNXrlZsI+WiiYI7Xl5vQm2
-+17DEuNeavtPAtDMxu8HmTg2UJ55Naxehbfe2lx2k5kYGGw3i1O1OVN2pZ2/OB71LucYd/5
-+qxPaz03wswcGOJYGPkNc40vdES/Scc7Yt8HsnZuzqkyOgzn0HiUCzoYUYLYTpLf+yGmwxS
-+yAEY056aOfkCsboKHOKiOmlJxNaZZFQkX1evep4DAAAFgC7HMbUuxzG1AAAAB3NzaC1yc2
-+EAAAGBALyudA9j1jnOIpMh1USUurQ4as4y+XCooryA+KdV/AWAlcPN1wBBji5yLVMlVxzQ
-+A5Yen6VatTWOgNiVbcoKDxemRcfKYeyFjJp4Q3Z8Sy8hrG4XdTNbYlyQ/0ft1pxh0SeDyq
-+0GrzC4Kh0a4nld0FVX4xUnqOo1/+E67j+mZryEzGVia12hqYcSf6T8pBHZDovAJilPRTb4
-+qI8j0tpzbY/Qf+t+SHYFr63e1X+WHLMPTEeeS1koncGvOekR+tmJei6RBkx5tZSYjJNbvP
-+CT0qr6IYWlJCypBaXEMHAdJu+ltSjspZSbJl/GzV65WbCPloomCO15eb0JttewxLjXmr7T
-+wLQzMbvB5k4NlCeeTWsXoW33tpcdpOZGBhsN4tTtTlTdqWdvzge9S7nGHf+asT2s9N8LMH
-+BjiWBj5DXONL3REv0nHO2LfB7J2bs6pMjoM59B4lAs6GFGC2E6S3/shpsMUsgBGNOemjn5
-+ArG6ChziojppScTWmWRUJF9Xr3qeAwAAAAMBAAEAAAGASj/kkEHbhbfmxzujL2/P4Sfqb+
-+aDXqAeGkwujbs6h/fH99vC5ejmSMTJrVSeaUo6fxLiBDIj6UWA0rpLEBzRP59BCpRL4MXV
-+RNxav/+9nniD4Hb+ug0WMhMlQmsH71ZW9lPYqCpfOq7ec8GmqdgPKeaCCEspH7HMVhfYtd
-+eHylwAC02lrpz1l5/h900sS5G9NaWR3uPA+xbzThDs4uZVkSidjlCNt1QZhDSSk7jA5n34
-+qJ5UTGu9WQDZqyxWKND+RIyQuFAPGQyoyCC1FayHO2sEhT5qHuumL14Mn81XpzoXFoKyql
-+rhBDe+pHhKArBYt92Evch0k1ABKblFxtxLXcvk4Fs7pHi+8k4+Cnazej2kcsu1kURlMZJB
-+w2QT/8BV4uImbH05LtyscQuwGzpIoxqrnHrvg5VbohStmhoOjYybzqqW3/M0qhkn5JgTiy
-+dJcHRJisRnAcmbmEchYtLDi6RW1e022H4I9AFXQqyr5HylBq6ugtWcFCsrcX8ibZ8xAAAA
-+wQCAOPgwae6yZLkrYzRfbxZtGKNmhpI0EtNSDCHYuQQapFZJe7EFENs/VAaIiiut0yajGj
-+c3aoKcwGIoT8TUM8E3GSNW6+WidUOC7H6W+/6N2OYZHRBACGz820xO+UBCl2oSk+dLBlfr
-+IQzBGUWn5uVYCs0/2nxfCdFyHtMK8dMF/ypbdG+o1rXz5y9b7PVG6Mn+o1Rjsdkq7VERmy
-+Pukd8hwATOIJqoKl3TuFyBeYFLqe+0e7uTeswQFw17PF31VjAAAADBAOpJRQb8c6qWqsvv
-+vkve0uMuL0DfWW0G6+SxjPLcV6aTWL5xu0Grd8uBxDkkHU/CDrAwpchXyuLsvbw21Eje/u
-+U5k9nLEscWZwcX7odxlK+EfAY2Bf5+Hd9bH5HMzTRJH8KkWK1EppOLPyiDxz4LZGzPLVyv
-+/1PgSuvXkSWk1KIE4SvSemyxGX2tPVI6uO+URqevfnPOS1tMB7BMQlgkR6eh4bugx9UYx9
-+mwlXonNa4dN0iQxZ7N4rKFBbT/uyB2bQAAAMEAzisnkD8k9Tn8uyhxpWLHwb03X4ZUUHDV
-+zu15e4a8dZ+mM8nHO986913Xz5JujlJKkGwFTvgWkIiR2zqTEauZHARH7gANpaweTm6lPd
-+E4p2S0M3ulY7xtp9lCFIrDhMPPkGq8SFZB6qhgucHcZSRLq6ZDou3S2IdNOzDTpBtkhRCS
-+0zFcdTLh3zZweoy8HGbW36bwB6s1CIL76Pd4F64i0Ms9CCCU6b+E5ArFhYQIsXiDbgHWbD
-+tZRSm2GEgnDGAvAAAACmphbWVzQG5ld3Q=
-+-----END OPENSSH PRIVATE KEY-----
-diff --git a/tests/integration_tests/assets/keys/id_rsa.test2.pub b/tests/integration_tests/assets/keys/id_rsa.test2.pub
-new file mode 100644
-index 00000000..f3831a57
---- /dev/null
-+++ b/tests/integration_tests/assets/keys/id_rsa.test2.pub
-@@ -0,0 +1 @@
-+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8rnQPY9Y5ziKTIdVElLq0OGrOMvlwqKK8gPinVfwFgJXDzdcAQY4uci1TJVcc0AOWHp+lWrU1joDYlW3KCg8XpkXHymHshYyaeEN2fEsvIaxuF3UzW2JckP9H7dacYdEng8qtBq8wuCodGuJ5XdBVV+MVJ6jqNf/hOu4/pma8hMxlYmtdoamHEn+k/KQR2Q6LwCYpT0U2+KiPI9Lac22P0H/rfkh2Ba+t3tV/lhyzD0xHnktZKJ3BrznpEfrZiXoukQZMebWUmIyTW7zwk9Kq+iGFpSQsqQWlxDBwHSbvpbUo7KWUmyZfxs1euVmwj5aKJgjteXm9CbbXsMS415q+08C0MzG7weZODZQnnk1rF6Ft97aXHaTmRgYbDeLU7U5U3alnb84HvUu5xh3/mrE9rPTfCzBwY4lgY+Q1zjS90RL9Jxzti3weydm7OqTI6DOfQeJQLOhhRgthOkt/7IabDFLIARjTnpo5+QKxugoc4qI6aUnE1plkVCRfV696ngM= test2@host
-diff --git a/tests/integration_tests/assets/keys/id_rsa.test3 b/tests/integration_tests/assets/keys/id_rsa.test3
-new file mode 100644
-index 00000000..2596c762
---- /dev/null
-+++ b/tests/integration_tests/assets/keys/id_rsa.test3
-@@ -0,0 +1,38 @@
-+-----BEGIN OPENSSH PRIVATE KEY-----
-+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
-+NhAAAAAwEAAQAAAYEApPG4MdkYQKD57/qreFrh9GRC22y66qZOWZWRjC887rrbvBzO69hV
-+yJpTIXleJEvpWiHYcjMR5G6NNFsnNtZ4fxDqmSc4vcFj53JsE/XNqLKq6psXadCb5vkNpG
-+bxA+Z5bJlzJ969PgJIIEbgc86sei4kgR2MuPWqtZbY5GkpNCTqWuLYeFK+14oFruA2nyWH
-+9MOIRDHK/d597psHy+LTMtymO7ZPhO571abKw6jvvwiSeDxVE9kV7KAQIuM9/S3gftvgQQ
-+ron3GL34pgmIabdSGdbfHqGDooryJhlbquJZELBN236KgRNTCAjVvUzjjQr1eRP3xssGwV
-+O6ECBGCQLl/aYogAgtwnwj9iXqtfiLK3EwlgjquU4+JQ0CVtLhG3gIZB+qoMThco0pmHTr
-+jtfQCwrztsBBFunSa2/CstuV1mQ5O5ZrZ6ACo9yPRBNkns6+CiKdtMtCtzi3k2RDz9jpYm
-+Pcak03Lr7IkdC1Tp6+jA+//yPHSO1o4CqW89IQzNAAAFgEUd7lZFHe5WAAAAB3NzaC1yc2
-+EAAAGBAKTxuDHZGECg+e/6q3ha4fRkQttsuuqmTlmVkYwvPO6627wczuvYVciaUyF5XiRL
-+6Voh2HIzEeRujTRbJzbWeH8Q6pknOL3BY+dybBP1zaiyquqbF2nQm+b5DaRm8QPmeWyZcy
-+fevT4CSCBG4HPOrHouJIEdjLj1qrWW2ORpKTQk6lri2HhSvteKBa7gNp8lh/TDiEQxyv3e
-+fe6bB8vi0zLcpju2T4Tue9WmysOo778Ikng8VRPZFeygECLjPf0t4H7b4EEK6J9xi9+KYJ
-+iGm3UhnW3x6hg6KK8iYZW6riWRCwTdt+ioETUwgI1b1M440K9XkT98bLBsFTuhAgRgkC5f
-+2mKIAILcJ8I/Yl6rX4iytxMJYI6rlOPiUNAlbS4Rt4CGQfqqDE4XKNKZh0647X0AsK87bA
-+QRbp0mtvwrLbldZkOTuWa2egAqPcj0QTZJ7OvgoinbTLQrc4t5NkQ8/Y6WJj3GpNNy6+yJ
-+HQtU6evowPv/8jx0jtaOAqlvPSEMzQAAAAMBAAEAAAGAGaqbdPZJNdVWzyb8g6/wtSzc0n
-+Qq6dSTIJGLonq/So69HpqFAGIbhymsger24UMGvsXBfpO/1wH06w68HWZmPa+OMeLOi4iK
-+WTuO4dQ/+l5DBlq32/lgKSLcIpb6LhcxEdsW9j9Mx1dnjc45owun/yMq/wRwH1/q/nLIsV
-+JD3R9ZcGcYNDD8DWIm3D17gmw+qbG7hJES+0oh4n0xS2KyZpm7LFOEMDVEA8z+hE/HbryQ
-+vjD1NC91n+qQWD1wKfN3WZDRwip3z1I5VHMpvXrA/spHpa9gzHK5qXNmZSz3/dfA1zHjCR
-+2dHjJnrIUH8nyPfw8t+COC+sQBL3Nr0KUWEFPRM08cOcQm4ctzg17aDIZBONjlZGKlReR8
-+1zfAw84Q70q2spLWLBLXSFblHkaOfijEbejIbaz2UUEQT27WD7RHAORdQlkx7eitk66T9d
-+DzIq/cpYhm5Fs8KZsh3PLldp9nsHbD2Oa9J9LJyI4ryuIW0mVwRdvPSiiYi3K+mDCpAAAA
-+wBe+ugEEJ+V7orb1f4Zez0Bd4FNkEc52WZL4CWbaCtM+ZBg5KnQ6xW14JdC8IS9cNi/I5P
-+yLsBvG4bWPLGgQruuKY6oLueD6BFnKjqF6ACUCiSQldh4BAW1nYc2U48+FFvo3ZQyudFSy
-+QEFlhHmcaNMDo0AIJY5Xnq2BG3nEX7AqdtZ8hhenHwLCRQJatDwSYBHDpSDdh9vpTnGp/2
-+0jBz25Ko4UANzvSAc3sA4yN3jfpoM366TgdNf8x3g1v7yljQAAAMEA0HSQjzH5nhEwB58k
-+mYYxnBYp1wb86zIuVhAyjZaeinvBQSTmLow8sXIHcCVuD3CgBezlU2SX5d9YuvRU9rcthi
-+uzn4wWnbnzYy4SwzkMJXchUAkumFVD8Hq5TNPh2Z+033rLLE08EhYypSeVpuzdpFoStaS9
-+3DUZA2bR/zLZI9MOVZRUcYImNegqIjOYHY8Sbj3/0QPV6+WpUJFMPvvedWhfaOsRMTA6nr
-+VLG4pxkrieVl0UtuRGbzD/exXhXVi7AAAAwQDKkJj4ez/+KZFYlZQKiV0BrfUFcgS6ElFM
-+2CZIEagCtu8eedrwkNqx2FUX33uxdvUTr4c9I3NvWeEEGTB9pgD4lh1x/nxfuhyGXtimFM
-+GnznGV9oyz0DmKlKiKSEGwWf5G+/NiiCwwVJ7wsQQm7TqNtkQ9b8MhWWXC7xlXKUs7dmTa
-+e8AqAndCCMEnbS1UQFO/R5PNcZXkFWDggLQ/eWRYKlrXgdnUgH6h0saOcViKpNJBUXb3+x
-+eauhOY52PS/BcAAAAKamFtZXNAbmV3dAE=
-+-----END OPENSSH PRIVATE KEY-----
-diff --git a/tests/integration_tests/assets/keys/id_rsa.test3.pub b/tests/integration_tests/assets/keys/id_rsa.test3.pub
-new file mode 100644
-index 00000000..057db632
---- /dev/null
-+++ b/tests/integration_tests/assets/keys/id_rsa.test3.pub
-@@ -0,0 +1 @@
-+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCk8bgx2RhAoPnv+qt4WuH0ZELbbLrqpk5ZlZGMLzzuutu8HM7r2FXImlMheV4kS+laIdhyMxHkbo00Wyc21nh/EOqZJzi9wWPncmwT9c2osqrqmxdp0Jvm+Q2kZvED5nlsmXMn3r0+AkggRuBzzqx6LiSBHYy49aq1ltjkaSk0JOpa4th4Ur7XigWu4DafJYf0w4hEMcr93n3umwfL4tMy3KY7tk+E7nvVpsrDqO+/CJJ4PFUT2RXsoBAi4z39LeB+2+BBCuifcYvfimCYhpt1IZ1t8eoYOiivImGVuq4lkQsE3bfoqBE1MICNW9TOONCvV5E/fGywbBU7oQIEYJAuX9piiACC3CfCP2Jeq1+IsrcTCWCOq5Tj4lDQJW0uEbeAhkH6qgxOFyjSmYdOuO19ALCvO2wEEW6dJrb8Ky25XWZDk7lmtnoAKj3I9EE2Sezr4KIp20y0K3OLeTZEPP2OliY9xqTTcuvsiR0LVOnr6MD7//I8dI7WjgKpbz0hDM0= test3@host
-diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
-new file mode 100644
-index 00000000..f82d7649
---- /dev/null
-+++ b/tests/integration_tests/modules/test_ssh_keysfile.py
-@@ -0,0 +1,85 @@
-+import paramiko
-+import pytest
-+from io import StringIO
-+from paramiko.ssh_exception import SSHException
-+
-+from tests.integration_tests.instances import IntegrationInstance
-+from tests.integration_tests.util import get_test_rsa_keypair
-+
-+TEST_USER1_KEYS = get_test_rsa_keypair('test1')
-+TEST_USER2_KEYS = get_test_rsa_keypair('test2')
-+TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
-+
-+USERDATA = """\
-+#cloud-config
-+bootcmd:
-+ - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
-+ssh_authorized_keys:
-+ - {default}
-+users:
-+- default
-+- name: test_user1
-+  ssh_authorized_keys:
-+    - {user1}
-+- name: test_user2
-+  ssh_authorized_keys:
-+    - {user2}
-+""".format(  # noqa: E501
-+    default=TEST_DEFAULT_KEYS.public_key,
-+    user1=TEST_USER1_KEYS.public_key,
-+    user2=TEST_USER2_KEYS.public_key,
-+)
-+
-+
-+@pytest.mark.ubuntu
-+@pytest.mark.user_data(USERDATA)
-+def test_authorized_keys(client: IntegrationInstance):
-+    expected_keys = [
-+        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
-+         TEST_USER1_KEYS),
-+        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
-+         TEST_USER2_KEYS),
-+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
-+         TEST_DEFAULT_KEYS),
-+        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
-+    ]
-+
-+    for user, filename, keys in expected_keys:
-+        contents = client.read_from_file(filename)
-+        if user in ['ubuntu', 'root']:
-+            # Our personal public key gets added by pycloudlib
-+            lines = contents.split('\n')
-+            assert len(lines) == 2
-+            assert keys.public_key.strip() in contents
-+        else:
-+            assert contents.strip() == keys.public_key.strip()
-+
-+        # Ensure we can actually connect
-+        ssh = paramiko.SSHClient()
-+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-+        paramiko_key = paramiko.RSAKey.from_private_key(StringIO(
-+            keys.private_key))
-+
-+        # Will fail with AuthenticationException if
-+        # we cannot connect
-+        ssh.connect(
-+            client.instance.ip,
-+            username=user,
-+            pkey=paramiko_key,
-+            look_for_keys=False,
-+            allow_agent=False,
-+        )
-+
-+        # Ensure other uses can't connect using our key
-+        other_users = [u[0] for u in expected_keys if u[2] != keys]
-+        for other_user in other_users:
-+            with pytest.raises(SSHException):
-+                print('trying to connect as {} with key from {}'.format(
-+                    other_user, user))
-+                ssh.connect(
-+                    client.instance.ip,
-+                    username=other_user,
-+                    pkey=paramiko_key,
-+                    look_for_keys=False,
-+                    allow_agent=False,
-+                )
-diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
-index fd1d1bac..bcb8044f 100644
---- a/tests/unittests/test_sshutil.py
-+++ b/tests/unittests/test_sshutil.py
-@@ -570,20 +570,33 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
-             ssh_util.render_authorizedkeysfile_paths(
-                 "%h/.keys", "/homedirs/bobby", "bobby"))
- 
-+    def test_all(self):
-+        self.assertEqual(
-+            ["/homedirs/bobby/.keys", "/homedirs/bobby/.secret/keys",
-+             "/keys/path1", "/opt/bobby/keys"],
-+            ssh_util.render_authorizedkeysfile_paths(
-+                "%h/.keys .secret/keys /keys/path1 /opt/%u/keys",
-+                "/homedirs/bobby", "bobby"))
-+
- 
- class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
- 
-     @patch("cloudinit.ssh_util.pwd.getpwnam")
-     def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby')
-+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
-         m_getpwnam.return_value = fpw
--        authorized_keys = self.tmp_path('authorized_keys')
-+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+
-+        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
-         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
- 
--        user_keys = self.tmp_path('user_keys')
-+        # /tmp/home2/bobby/.ssh/user_keys = dsa
-+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
-         util.write_file(user_keys, VALID_CONTENT['dsa'])
- 
--        sshd_config = self.tmp_path('sshd_config')
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-         util.write_file(
-             sshd_config,
-             "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
-@@ -593,33 +606,244 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-             fpw.pw_name, sshd_config)
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
--        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
-+        self.assertEqual(user_keys, auth_key_fn)
-         self.assertTrue(VALID_CONTENT['rsa'] in content)
-         self.assertTrue(VALID_CONTENT['dsa'] in content)
- 
-     @patch("cloudinit.ssh_util.pwd.getpwnam")
-     def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
--        fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie')
-+        fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
-         m_getpwnam.return_value = fpw
--        authorized_keys = self.tmp_path('authorized_keys')
-+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+
-+        # /tmp/home/suzie/.ssh/authorized_keys = rsa
-+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
-         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
- 
--        user_keys = self.tmp_path('user_keys')
-+        # /tmp/home/suzie/.ssh/user_keys = dsa
-+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
-         util.write_file(user_keys, VALID_CONTENT['dsa'])
- 
--        sshd_config = self.tmp_path('sshd_config')
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-         util.write_file(
-             sshd_config,
--            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
-+            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
-         )
- 
-         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
--            fpw.pw_name, sshd_config
-+            fpw.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        self.assertEqual(authorized_keys, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['rsa'] in content)
-+        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam):
-+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
-+        m_getpwnam.return_value = fpw
-+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+
-+        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
-+        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
-+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+
-+        # /tmp/home2/bobby/.ssh/user_keys = dsa
-+        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
-+        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+
-+        # /tmp/etc/ssh/authorized_keys = ecdsa
-+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
-+                                               dir="/tmp")
-+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-+        util.write_file(
-+            sshd_config,
-+            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
-+                                             user_keys, authorized_keys)
-+        )
-+
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        self.assertEqual(authorized_keys, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['rsa'] in content)
-+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
-+        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam):
-+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
-+        m_getpwnam.return_value = fpw
-+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+
-+        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
-+        authorized_keys = self.tmp_path('authorized_keys2',
-+                                        dir=user_ssh_folder)
-+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+
-+        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
-+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
-+        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+
-+        # /tmp/etc/ssh/authorized_keys = ecdsa
-+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
-+                                               dir="/tmp")
-+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-+        util.write_file(
-+            sshd_config,
-+            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
-+                                             authorized_keys, user_keys)
-+        )
-+
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        self.assertEqual(user_keys, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['rsa'] in content)
-+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
-+        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    def test_multiple_authorizedkeys_file_global(self, m_getpwnam):
-+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
-+        m_getpwnam.return_value = fpw
-+
-+        # /tmp/etc/ssh/authorized_keys = rsa
-+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
-+                                               dir="/tmp")
-+        util.write_file(authorized_keys_global, VALID_CONTENT['rsa'])
-+
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config')
-+        util.write_file(
-+            sshd_config,
-+            "AuthorizedKeysFile %s" % (authorized_keys_global)
-         )
-+
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw.pw_name, sshd_config)
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
-         self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
-         self.assertTrue(VALID_CONTENT['rsa'] in content)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam):
-+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
-+        m_getpwnam.return_value = fpw
-+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
-+        authorized_keys = self.tmp_path('authorized_keys2',
-+                                        dir=user_ssh_folder)
-+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
-+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
-+        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+
-+        fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
-+        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
-+        # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com
-+        authorized_keys2 = self.tmp_path('authorized_keys2',
-+                                         dir=user_ssh_folder)
-+        util.write_file(authorized_keys2,
-+                        VALID_CONTENT['ssh-xmss@openssh.com'])
-+
-+        # /tmp/etc/ssh/authorized_keys = ecdsa
-+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
-+                                               dir="/tmp")
-+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-+        util.write_file(
-+            sshd_config,
-+            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" %
-+            (authorized_keys_global, user_keys)
-+        )
-+
-+        # process first user
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        self.assertEqual(user_keys, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['rsa'] in content)
-+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
-+        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
-+
-+        m_getpwnam.return_value = fpw2
-+        # process second user
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw2.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        self.assertEqual(authorized_keys2, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
-+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
-+        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        self.assertFalse(VALID_CONTENT['rsa'] in content)
-+
-+    @patch("cloudinit.ssh_util.pwd.getpwnam")
-+    def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam):
-+        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby')
-+        m_getpwnam.return_value = fpw
-+        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
-+        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
-+        authorized_keys = self.tmp_path('authorized_keys2',
-+                                        dir=user_ssh_folder)
-+        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
-+        # /tmp/home/bobby/.ssh/user_keys3 = dsa
-+        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
-+        util.write_file(user_keys, VALID_CONTENT['dsa'])
-+
-+        fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy')
-+        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
-+        # /tmp/home/badguy/home/bobby = ""
-+        authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy")
-+
-+        # /tmp/etc/ssh/authorized_keys = ecdsa
-+        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
-+                                               dir="/tmp")
-+        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
-+
-+        # /tmp/sshd_config
-+        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
-+        util.write_file(
-+            sshd_config,
-+            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" %
-+            (authorized_keys_global, user_keys, authorized_keys2)
-+        )
-+
-+        # process first user
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        self.assertEqual(user_keys, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['rsa'] in content)
-+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
-+        self.assertTrue(VALID_CONTENT['dsa'] in content)
-+
-+        m_getpwnam.return_value = fpw2
-+        # process second user
-+        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-+            fpw2.pw_name, sshd_config)
-+        content = ssh_util.update_authorized_keys(auth_key_entries, [])
-+
-+        # badguy should not take the key from the other user!
-+        self.assertEqual(authorized_keys2, auth_key_fn)
-+        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
-         self.assertTrue(VALID_CONTENT['dsa'] in content)
-+        self.assertFalse(VALID_CONTENT['rsa'] in content)
- 
- # vi: ts=4 expandtab
--- 
-2.27.0
-
diff --git a/SOURCES/ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch b/SOURCES/ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch
deleted file mode 100644
index 13484d3..0000000
--- a/SOURCES/ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch
+++ /dev/null
@@ -1,85 +0,0 @@
-From 7d4e16bfc1cefbdd4d1477480b02b1d6c1399e4d Mon Sep 17 00:00:00 2001
-From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date: Mon, 20 Sep 2021 12:16:36 +0200
-Subject: [PATCH] ssh_utils.py: ignore when sshd_config options are not
- key/value pairs (#1007)
-
-RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-MergeRequest: 31: ssh_utils.py: ignore when sshd_config options are not key/value pairs (#1007)
-RH-Commit: [1/1] 9007fb8a116e98036ff17df0168a76e9a5843671 (eesposit/cloud-init)
-RH-Bugzilla: 1862933
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
-
-TESTED: by me
-BREW: 39832462
-
-commit 2ce857248162957a785af61c135ca8433fdbbcde
-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-Date:   Wed Sep 8 02:08:36 2021 +0200
-
-    ssh_utils.py: ignore when sshd_config options are not key/value pairs (#1007)
-
-    As specified in #LP 1845552,
-    In cloudinit/ssh_util.py, in parse_ssh_config_lines(), we attempt to
-    parse each line of sshd_config. This function expects each line to
-    be one of the following forms:
-
-        \# comment
-        key value
-        key=value
-
-    However, options like DenyGroups and DenyUsers are specified to
-    *optionally* accepts values in sshd_config.
-    Cloud-init should comply to this and skip the option if a value
-    is not provided.
-
-    Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-
-Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
----
- cloudinit/ssh_util.py           | 8 +++++++-
- tests/unittests/test_sshutil.py | 8 ++++++++
- 2 files changed, 15 insertions(+), 1 deletion(-)
-
-diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
-index 9ccadf09..33679dcc 100644
---- a/cloudinit/ssh_util.py
-+++ b/cloudinit/ssh_util.py
-@@ -484,7 +484,13 @@ def parse_ssh_config_lines(lines):
-         try:
-             key, val = line.split(None, 1)
-         except ValueError:
--            key, val = line.split('=', 1)
-+            try:
-+                key, val = line.split('=', 1)
-+            except ValueError:
-+                LOG.debug(
-+                    "sshd_config: option \"%s\" has no key/value pair,"
-+                    " skipping it", line)
-+                continue
-         ret.append(SshdConfigLine(line, key, val))
-     return ret
- 
-diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
-index a66788bf..08e20050 100644
---- a/tests/unittests/test_sshutil.py
-+++ b/tests/unittests/test_sshutil.py
-@@ -525,6 +525,14 @@ class TestUpdateSshConfigLines(test_helpers.CiTestCase):
-         self.assertEqual([self.pwauth], result)
-         self.check_line(lines[-1], self.pwauth, "no")
- 
-+    def test_option_without_value(self):
-+        """Implementation only accepts key-value pairs."""
-+        extended_exlines = self.exlines.copy()
-+        denyusers_opt = "DenyUsers"
-+        extended_exlines.append(denyusers_opt)
-+        lines = ssh_util.parse_ssh_config_lines(list(extended_exlines))
-+        self.assertNotIn(denyusers_opt, str(lines))
-+
-     def test_single_option_updated(self):
-         """A single update should have change made and line updated."""
-         opt, val = ("UsePAM", "no")
--- 
-2.27.0
-
diff --git a/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch b/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch
deleted file mode 100644
index 5cf4671..0000000
--- a/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch
+++ /dev/null
@@ -1,369 +0,0 @@
-From 769b9f8c9b1ecc294a197575108ae7cb54ad7f4b Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Jul 2021 14:13:45 +0200
-Subject: [PATCH] write passwords only to serial console, lock down
- cloud-init-output.log (#847)
-
-RH-Author: Eduardo Otubo <otubo@redhat.com>
-RH-MergeRequest: 21: write passwords only to serial console, lock down cloud-init-output.log (#847)
-RH-Commit: [1/1] 8f30f2b7d0d6f9dca19994dbd0827b44e998f238 (otubo/cloud-init)
-RH-Bugzilla: 1945891
-RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
-RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
-
-commit b794d426b9ab43ea9d6371477466070d86e10668
-Author: Daniel Watkins <oddbloke@ubuntu.com>
-Date:   Fri Mar 19 10:06:42 2021 -0400
-
-    write passwords only to serial console, lock down cloud-init-output.log (#847)
-
-    Prior to this commit, when a user specified configuration which would
-    generate random passwords for users, cloud-init would cause those
-    passwords to be written to the serial console by emitting them on
-    stderr.  In the default configuration, any stdout or stderr emitted by
-    cloud-init is also written to `/var/log/cloud-init-output.log`.  This
-    file is world-readable, meaning that those randomly-generated passwords
-    were available to be read by any user with access to the system.  This
-    presents an obvious security issue.
-
-    This commit responds to this issue in two ways:
-
-    * We address the direct issue by moving from writing the passwords to
-      sys.stderr to writing them directly to /dev/console (via
-      util.multi_log); this means that the passwords will never end up in
-      cloud-init-output.log
-    * To avoid future issues like this, we also modify the logging code so
-      that any files created in a log sink subprocess will only be
-      owner/group readable and, if it exists, will be owned by the adm
-      group.  This results in `/var/log/cloud-init-output.log` no longer
-      being world-readable, meaning that if there are other parts of the
-      codebase that are emitting sensitive data intended for the serial
-      console, that data is no longer available to all users of the system.
-
-    LP: #1918303
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/config/cc_set_passwords.py          |  5 +-
- cloudinit/config/tests/test_set_passwords.py  | 40 +++++++++----
- cloudinit/tests/test_util.py                  | 56 +++++++++++++++++++
- cloudinit/util.py                             | 38 +++++++++++--
- .../modules/test_set_password.py              | 24 ++++++++
- tests/integration_tests/test_logging.py       | 22 ++++++++
- tests/unittests/test_util.py                  |  4 ++
- 7 files changed, 173 insertions(+), 16 deletions(-)
- create mode 100644 tests/integration_tests/test_logging.py
-
-diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
-index d6b5682d..433de751 100755
---- a/cloudinit/config/cc_set_passwords.py
-+++ b/cloudinit/config/cc_set_passwords.py
-@@ -78,7 +78,6 @@ password.
- """
- 
- import re
--import sys
- 
- from cloudinit.distros import ug_util
- from cloudinit import log as logging
-@@ -214,7 +213,9 @@ def handle(_name, cfg, cloud, log, args):
-         if len(randlist):
-             blurb = ("Set the following 'random' passwords\n",
-                      '\n'.join(randlist))
--            sys.stderr.write("%s\n%s\n" % blurb)
-+            util.multi_log(
-+                "%s\n%s\n" % blurb, stderr=False, fallback_to_stdout=False
-+            )
- 
-         if expire:
-             expired_users = []
-diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py
-index daa1ef51..bbe2ee8f 100644
---- a/cloudinit/config/tests/test_set_passwords.py
-+++ b/cloudinit/config/tests/test_set_passwords.py
-@@ -74,10 +74,6 @@ class TestSetPasswordsHandle(CiTestCase):
- 
-     with_logs = True
- 
--    def setUp(self):
--        super(TestSetPasswordsHandle, self).setUp()
--        self.add_patch('cloudinit.config.cc_set_passwords.sys.stderr', 'm_err')
--
-     def test_handle_on_empty_config(self, *args):
-         """handle logs that no password has changed when config is empty."""
-         cloud = self.tmp_cloud(distro='ubuntu')
-@@ -129,10 +125,12 @@ class TestSetPasswordsHandle(CiTestCase):
-             mock.call(['pw', 'usermod', 'ubuntu', '-p', '01-Jan-1970'])],
-             m_subp.call_args_list)
- 
-+    @mock.patch(MODPATH + "util.multi_log")
-     @mock.patch(MODPATH + "util.is_BSD")
-     @mock.patch(MODPATH + "subp.subp")
--    def test_handle_on_chpasswd_list_creates_random_passwords(self, m_subp,
--                                                              m_is_bsd):
-+    def test_handle_on_chpasswd_list_creates_random_passwords(
-+        self, m_subp, m_is_bsd, m_multi_log
-+    ):
-         """handle parses command set random passwords."""
-         m_is_bsd.return_value = False
-         cloud = self.tmp_cloud(distro='ubuntu')
-@@ -146,10 +144,32 @@ class TestSetPasswordsHandle(CiTestCase):
-         self.assertIn(
-             'DEBUG: Handling input for chpasswd as list.',
-             self.logs.getvalue())
--        self.assertNotEqual(
--            [mock.call(['chpasswd'],
--             '\n'.join(valid_random_pwds) + '\n')],
--            m_subp.call_args_list)
-+
-+        self.assertEqual(1, m_subp.call_count)
-+        args, _kwargs = m_subp.call_args
-+        self.assertEqual(["chpasswd"], args[0])
-+
-+        stdin = args[1]
-+        user_pass = {
-+            user: password
-+            for user, password
-+            in (line.split(":") for line in stdin.splitlines())
-+        }
-+
-+        self.assertEqual(1, m_multi_log.call_count)
-+        self.assertEqual(
-+            mock.call(mock.ANY, stderr=False, fallback_to_stdout=False),
-+            m_multi_log.call_args
-+        )
-+
-+        self.assertEqual(set(["root", "ubuntu"]), set(user_pass.keys()))
-+        written_lines = m_multi_log.call_args[0][0].splitlines()
-+        for password in user_pass.values():
-+            for line in written_lines:
-+                if password in line:
-+                    break
-+            else:
-+                self.fail("Password not emitted to console")
- 
- 
- # vi: ts=4 expandtab
-diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
-index b7a302f1..e811917e 100644
---- a/cloudinit/tests/test_util.py
-+++ b/cloudinit/tests/test_util.py
-@@ -851,4 +851,60 @@ class TestEnsureFile:
-         assert "ab" == kwargs["omode"]
- 
- 
-+@mock.patch("cloudinit.util.grp.getgrnam")
-+@mock.patch("cloudinit.util.os.setgid")
-+@mock.patch("cloudinit.util.os.umask")
-+class TestRedirectOutputPreexecFn:
-+    """This tests specifically the preexec_fn used in redirect_output."""
-+
-+    @pytest.fixture(params=["outfmt", "errfmt"])
-+    def preexec_fn(self, request):
-+        """A fixture to gather the preexec_fn used by redirect_output.
-+
-+        This enables simpler direct testing of it, and parameterises any tests
-+        using it to cover both the stdout and stderr code paths.
-+        """
-+        test_string = "| piped output to invoke subprocess"
-+        if request.param == "outfmt":
-+            args = (test_string, None)
-+        elif request.param == "errfmt":
-+            args = (None, test_string)
-+        with mock.patch("cloudinit.util.subprocess.Popen") as m_popen:
-+            util.redirect_output(*args)
-+
-+        assert 1 == m_popen.call_count
-+        _args, kwargs = m_popen.call_args
-+        assert "preexec_fn" in kwargs, "preexec_fn not passed to Popen"
-+        return kwargs["preexec_fn"]
-+
-+    def test_preexec_fn_sets_umask(
-+        self, m_os_umask, _m_setgid, _m_getgrnam, preexec_fn
-+    ):
-+        """preexec_fn should set a mask that avoids world-readable files."""
-+        preexec_fn()
-+
-+        assert [mock.call(0o037)] == m_os_umask.call_args_list
-+
-+    def test_preexec_fn_sets_group_id_if_adm_group_present(
-+        self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn
-+    ):
-+        """We should setgrp to adm if present, so files are owned by them."""
-+        fake_group = mock.Mock(gr_gid=mock.sentinel.gr_gid)
-+        m_getgrnam.return_value = fake_group
-+
-+        preexec_fn()
-+
-+        assert [mock.call("adm")] == m_getgrnam.call_args_list
-+        assert [mock.call(mock.sentinel.gr_gid)] == m_setgid.call_args_list
-+
-+    def test_preexec_fn_handles_absent_adm_group_gracefully(
-+        self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn
-+    ):
-+        """We should handle an absent adm group gracefully."""
-+        m_getgrnam.side_effect = KeyError("getgrnam(): name not found: 'adm'")
-+
-+        preexec_fn()
-+
-+        assert 0 == m_setgid.call_count
-+
- # vi: ts=4 expandtab
-diff --git a/cloudinit/util.py b/cloudinit/util.py
-index 769f3425..4e0a72db 100644
---- a/cloudinit/util.py
-+++ b/cloudinit/util.py
-@@ -359,7 +359,7 @@ def find_modules(root_dir):
- 
- 
- def multi_log(text, console=True, stderr=True,
--              log=None, log_level=logging.DEBUG):
-+              log=None, log_level=logging.DEBUG, fallback_to_stdout=True):
-     if stderr:
-         sys.stderr.write(text)
-     if console:
-@@ -368,7 +368,7 @@ def multi_log(text, console=True, stderr=True,
-             with open(conpath, 'w') as wfh:
-                 wfh.write(text)
-                 wfh.flush()
--        else:
-+        elif fallback_to_stdout:
-             # A container may lack /dev/console (arguably a container bug).  If
-             # it does not exist, then write output to stdout.  this will result
-             # in duplicate stderr and stdout messages if stderr was True.
-@@ -623,6 +623,26 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
-     if not o_err:
-         o_err = sys.stderr
- 
-+    # pylint: disable=subprocess-popen-preexec-fn
-+    def set_subprocess_umask_and_gid():
-+        """Reconfigure umask and group ID to create output files securely.
-+
-+        This is passed to subprocess.Popen as preexec_fn, so it is executed in
-+        the context of the newly-created process.  It:
-+
-+        * sets the umask of the process so created files aren't world-readable
-+        * if an adm group exists in the system, sets that as the process' GID
-+          (so that the created file(s) are owned by root:adm)
-+        """
-+        os.umask(0o037)
-+        try:
-+            group_id = grp.getgrnam("adm").gr_gid
-+        except KeyError:
-+            # No adm group, don't set a group
-+            pass
-+        else:
-+            os.setgid(group_id)
-+
-     if outfmt:
-         LOG.debug("Redirecting %s to %s", o_out, outfmt)
-         (mode, arg) = outfmt.split(" ", 1)
-@@ -632,7 +652,12 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
-                 owith = "wb"
-             new_fp = open(arg, owith)
-         elif mode == "|":
--            proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE)
-+            proc = subprocess.Popen(
-+                arg,
-+                shell=True,
-+                stdin=subprocess.PIPE,
-+                preexec_fn=set_subprocess_umask_and_gid,
-+            )
-             new_fp = proc.stdin
-         else:
-             raise TypeError("Invalid type for output format: %s" % outfmt)
-@@ -654,7 +679,12 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
-                 owith = "wb"
-             new_fp = open(arg, owith)
-         elif mode == "|":
--            proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE)
-+            proc = subprocess.Popen(
-+                arg,
-+                shell=True,
-+                stdin=subprocess.PIPE,
-+                preexec_fn=set_subprocess_umask_and_gid,
-+            )
-             new_fp = proc.stdin
-         else:
-             raise TypeError("Invalid type for error format: %s" % errfmt)
-diff --git a/tests/integration_tests/modules/test_set_password.py b/tests/integration_tests/modules/test_set_password.py
-index b13f76fb..d7cf91a5 100644
---- a/tests/integration_tests/modules/test_set_password.py
-+++ b/tests/integration_tests/modules/test_set_password.py
-@@ -116,6 +116,30 @@ class Mixin:
-         # Which are not the same
-         assert shadow_users["harry"] != shadow_users["dick"]
- 
-+    def test_random_passwords_not_stored_in_cloud_init_output_log(
-+        self, class_client
-+    ):
-+        """We should not emit passwords to the in-instance log file.
-+
-+        LP: #1918303
-+        """
-+        cloud_init_output = class_client.read_from_file(
-+            "/var/log/cloud-init-output.log"
-+        )
-+        assert "dick:" not in cloud_init_output
-+        assert "harry:" not in cloud_init_output
-+
-+    def test_random_passwords_emitted_to_serial_console(self, class_client):
-+        """We should emit passwords to the serial console. (LP: #1918303)"""
-+        try:
-+            console_log = class_client.instance.console_log()
-+        except NotImplementedError:
-+            # Assume that an exception here means that we can't use the console
-+            # log
-+            pytest.skip("NotImplementedError when requesting console log")
-+        assert "dick:" in console_log
-+        assert "harry:" in console_log
-+
-     def test_explicit_password_set_correctly(self, class_client):
-         """Test that an explicitly-specified password is set correctly."""
-         shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client)
-diff --git a/tests/integration_tests/test_logging.py b/tests/integration_tests/test_logging.py
-new file mode 100644
-index 00000000..b31a0434
---- /dev/null
-+++ b/tests/integration_tests/test_logging.py
-@@ -0,0 +1,22 @@
-+"""Integration tests relating to cloud-init's logging."""
-+
-+
-+class TestVarLogCloudInitOutput:
-+    """Integration tests relating to /var/log/cloud-init-output.log."""
-+
-+    def test_var_log_cloud_init_output_not_world_readable(self, client):
-+        """
-+        The log can contain sensitive data, it shouldn't be world-readable.
-+
-+        LP: #1918303
-+        """
-+        # Check the file exists
-+        assert client.execute("test -f /var/log/cloud-init-output.log").ok
-+
-+        # Check its permissions are as we expect
-+        perms, user, group = client.execute(
-+            "stat -c %a:%U:%G /var/log/cloud-init-output.log"
-+        ).split(":")
-+        assert "640" == perms
-+        assert "root" == user
-+        assert "adm" == group
-diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
-index 857629f1..e5292001 100644
---- a/tests/unittests/test_util.py
-+++ b/tests/unittests/test_util.py
-@@ -572,6 +572,10 @@ class TestMultiLog(helpers.FilesystemMockingTestCase):
-         util.multi_log(logged_string)
-         self.assertEqual(logged_string, self.stdout.getvalue())
- 
-+    def test_logs_dont_go_to_stdout_if_fallback_to_stdout_is_false(self):
-+        util.multi_log('something', fallback_to_stdout=False)
-+        self.assertEqual('', self.stdout.getvalue())
-+
-     def test_logs_go_to_log_if_given(self):
-         log = mock.MagicMock()
-         logged_string = 'something very important'
--- 
-2.27.0
-
diff --git a/SOURCES/test_version_change.pkl b/SOURCES/test_version_change.pkl
deleted file mode 100644
index 65ae93e..0000000
Binary files a/SOURCES/test_version_change.pkl and /dev/null differ
diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec
index 04b8907..bc0729b 100644
--- a/SPECS/cloud-init.spec
+++ b/SPECS/cloud-init.spec
@@ -5,8 +5,8 @@
 %global debug_package %{nil}
 
 Name:           cloud-init
-Version:        21.1
-Release:        15%{?dist}
+Version:        22.1
+Release:        1%{?dist}
 Summary:        Cloud instance init scripts
 
 Group:          System Environment/Base
@@ -14,66 +14,13 @@ License:        GPLv3
 URL:            http://launchpad.net/cloud-init
 Source0:        https://launchpad.net/cloud-init/trunk/%{version}/+download/%{name}-%{version}.tar.gz
 Source1:        cloud-init-tmpfiles.conf
-Source2:        test_version_change.pkl
 
 Patch0001: 0001-Add-initial-redhat-setup.patch
 Patch0002: 0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
 Patch0003: 0003-limit-permissions-on-def_log_file.patch
-Patch0004: 0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
-Patch0005: 0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
-Patch0006: 0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
-Patch0007: 0007-Remove-race-condition-between-cloud-init-and-Network.patch
-Patch0008: 0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch
-Patch0009: 0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch
-# For bz#1957532 - [cloud-init] From RHEL 82+ cloud-init no longer displays sshd keys fingerprints from instance launched from a backup image
-Patch10: ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
-# For bz#1945891 - CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-8]
-Patch11: ci-write-passwords-only-to-serial-console-lock-down-clo.patch
-# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure
-Patch12: ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch
-# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure
-Patch13: ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch
-# For bz#1995840 - [cloudinit]  Fix home permissions modified by ssh module
-Patch14: ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch
-# For bz#1862933 - cloud-init fails with ValueError: need more than 1 value to unpack[rhel-8]
-Patch15: ci-ssh_utils.py-ignore-when-sshd_config-options-are-not.patch
-# For bz#2013644 - cloud-init fails to set host key permissions correctly
-Patch16: ci-cc_ssh.py-fix-private-key-group-owner-and-permission.patch
-# For bz#2021538 - cloud-init.service fails to start after package update
-Patch17: ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch
-# For bz#2028028 - [RHEL-8] Above 19.2 of cloud-init fails to configure routes when configuring static and default routes to the same destination IP
-Patch18: ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch
-# For bz#2039697 - [RHEL8] [Azure] cloud-init fails to configure the system
-# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo'
-Patch20: ci-Datasource-for-VMware-953.patch
-# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo'
-Patch21: ci-Change-netifaces-dependency-to-0.10.4-965.patch
-# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo'
-Patch22: ci-Update-dscheck_VMware-s-rpctool-check-970.patch
-# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo'
-Patch23: ci-Revert-unnecesary-lcase-in-ds-identify-978.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch24: ci-Add-flexibility-to-IMDS-api-version-793.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch25: ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch26: ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch27: ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch28: ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch29: ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch
-# For bz#2023940 - [RHEL-8] Support for provisioning Azure VM with userdata
-Patch30: ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch
-# For bz#2046540 - cloud-init writes route6-$DEVICE config with a HEX netmask. ip route does not like : Error: inet6 prefix is expected rather than "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::".
-Patch31: ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
-# For bz#1935826 - [rhel-8] Cloud-init init stage fails after upgrade from RHEL7 to RHEL8.
-Patch32: ci-Detect-a-Python-version-change-and-clear-the-cache-8.patch
-# For bz#1935826 - [rhel-8] Cloud-init init stage fails after upgrade from RHEL7 to RHEL8.
-Patch33: ci-Fix-MIME-policy-failure-on-python-version-upgrade-93.patch
-# For bz#2026587 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo'
-
+Patch0004: 0004-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
+Patch0005: 0005-Remove-race-condition-between-cloud-init-and-Network.patch
+Patch0006: 0006-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
 
 BuildArch:      noarch
 
@@ -142,8 +89,6 @@ ssh keys and to let the user run various scripts.
 sed -i -e 's|#!/usr/bin/env python|#!/usr/bin/env python3|' \
        -e 's|#!/usr/bin/python|#!/usr/bin/python3|' tools/* cloudinit/ssh_util.py
 
-cp -f %{SOURCE2} tests/integration_tests/assets/test_version_change.pkl
-
 %build
 %py3_build
 
@@ -265,17 +210,21 @@ fi
 %{_bindir}/cloud-id
 %{_libexecdir}/%{name}/ds-identify
 /usr/lib/systemd/system-generators/cloud-init-generator
+%{_sysconfdir}/systemd/system/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf
 
 
 %dir %{_sysconfdir}/rsyslog.d
 %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf
 
 %changelog
+* Mon Apr 25 2022 Amy Chen <xiachen@redhat.com> - 22.1-1
+- Rebaes to 22.1 [bz#2065544]
+- Resolves: bz#2065544
+  ([RHEL-8.7.0] cloud-init rebase to 22.1)
+
 * Fri Apr 01 2022 Camilla Conte <cconte@redhat.com> - 21.1-15
 - ci-Detect-a-Python-version-change-and-clear-the-cache-8.patch [bz#1935826]
 - ci-Fix-MIME-policy-failure-on-python-version-upgrade-93.patch [bz#1935826]
-- Resolves: bz#1935826
-  ([rhel-8] Cloud-init init stage fails after upgrade from RHEL7 to RHEL8.)
 
 * Fri Feb 25 2022 Jon Maloy <jmaloy@redhat.com> - 21.1-14
 - ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch [bz#2046540]
@@ -363,6 +312,17 @@ fi
 - Resolves: bz#1958174
   ([RHEL-8.5.0] Rebase cloud-init to 21.1)
 
+* Thu May 13 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8_4.3
+- ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch [bz#1957135]
+- ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch [bz#1957135]
+- Resolves: bz#1957135
+  (Intermittent failure to start cloud-init due to failure to detect macs [rhel-8.4.0.z])
+
+* Tue Apr 06 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8_4.1
+- ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch [bz#1942699]
+- Resolves: bz#1942699
+  ([Aliyun][RHEL8.4][cloud-init] cloud-init service failed to start with Alibaba instance [rhel-8.4.0.z])
+
 * Tue Feb 02 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8
 - ci-fix-a-typo-in-man-page-cloud-init.1-752.patch [bz#1913127]
 - Resolves: bz#1913127