From 5c6b94f489958b6c858fcd70c0e5c4b141841cb8 Mon Sep 17 00:00:00 2001 From: akutz Date: Fri, 28 Feb 2025 10:17:28 -0600 Subject: [PATCH] feat(vmware): Support network events This patch updates the DataSource for VMware to support network reconfiguration when the BOOT, BOOT_LEGACY, BOOT_NEW_INSTANCE, and HOTPLUG events are received. Previously the datasource could only reconfigure the network if a new instance ID was detected. However, due to features like backup/restore, migrating VMs, etc., it was determined that it is valuable to support reconfiguring the network without changing the instance ID. This is because changing the instance ID also means running the per-instance configuration modules, such as regenerating the system's SSH host keys, which could lock out automation. This patch also indicates the datasource supports network reconfig for HOTPLUG events, such as when a new NIC is added to a VM. --- cloudinit/sources/DataSourceVMware.py | 33 ++++++++ tests/unittests/sources/test_vmware.py | 103 +++++++++++++++++++++++++ tests/unittests/test_upgrade.py | 2 + 3 files changed, 138 insertions(+) diff --git a/cloudinit/sources/DataSourceVMware.py b/cloudinit/sources/DataSourceVMware.py index d69f3673185..d9c223be1b1 100644 --- a/cloudinit/sources/DataSourceVMware.py +++ b/cloudinit/sources/DataSourceVMware.py @@ -28,6 +28,7 @@ import time from cloudinit import atomic_helper, dmi, net, netinfo, sources, util +from cloudinit.event import EventScope, EventType from cloudinit.log import loggers from cloudinit.sources.helpers.vmware.imc import guestcust_util from cloudinit.subp import ProcessExecutionError, subp, which @@ -53,6 +54,18 @@ WAIT_ON_NETWORK_IPV4 = "ipv4" WAIT_ON_NETWORK_IPV6 = "ipv6" +SUPPORTED_UPDATE_EVENTS = { + # Support network reconfiguration for the following events: + EventScope.NETWORK: { + EventType.BOOT, + EventType.BOOT_LEGACY, + EventType.BOOT_NEW_INSTANCE, + EventType.HOTPLUG, + # TODO(akutz) Add support for METADATA_CHANGE and USER_REQUEST when + # those events are supported by Cloud-Init. + } +} + class DataSourceVMware(sources.DataSource): """ @@ -93,6 +106,8 @@ class DataSourceVMware(sources.DataSource): dsname = "VMware" + supported_update_events = copy.deepcopy(SUPPORTED_UPDATE_EVENTS) + def __init__(self, sys_cfg, distro, paths, ud_proc=None): sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc) @@ -101,6 +116,12 @@ def __init__(self, sys_cfg, distro, paths, ud_proc=None): self.rpctool = None self.rpctool_fn = None + # The default list of configured update events should be the same as + # the list of supported update events. + self.default_update_events = copy.deepcopy( + self.supported_update_events + ) + # A list includes all possible data transports, each tuple represents # one data transport type. This datasource will try to get data from # each of transports follows the tuples order in this list. @@ -121,6 +142,18 @@ def _unpickle(self, ci_pkl_version: int) -> None: setattr(self, attr, None) if not hasattr(self, "cfg"): setattr(self, "cfg", {}) + if not hasattr(self, "supported_update_events"): + setattr( + self, + "supported_update_events", + copy.deepcopy(SUPPORTED_UPDATE_EVENTS), + ) + if not hasattr(self, "default_update_events"): + setattr( + self, + "default_update_events", + copy.deepcopy(self.supported_update_events), + ) if not hasattr(self, "possible_data_access_method_list"): setattr( self, diff --git a/tests/unittests/sources/test_vmware.py b/tests/unittests/sources/test_vmware.py index cfeff6d53a7..fc4c0a066a8 100644 --- a/tests/unittests/sources/test_vmware.py +++ b/tests/unittests/sources/test_vmware.py @@ -14,6 +14,7 @@ import pytest from cloudinit import dmi, helpers, safeyaml, settings, util +from cloudinit.event import EventScope, EventType from cloudinit.sources import DataSourceVMware from cloudinit.sources.helpers.vmware.imc import guestcust_util from cloudinit.subp import ProcessExecutionError @@ -106,6 +107,13 @@ "addr": "fd42:baa2:3dd:17a:216:3eff:fe16:db54", } +VMW_EXPECTED_DEFAULT_EVENTS = { + EventType.BOOT, + EventType.BOOT_LEGACY, + EventType.BOOT_NEW_INSTANCE, + EventType.HOTPLUG, +} + def generate_test_netdev_data(ipv4=None, ipv6=None): ipv4 = ipv4 or [] @@ -469,6 +477,16 @@ def test_get_subplatform(self, m_fn): ), ) + # Test to ensure that network is configured from metadata on each boot. + self.assertSetEqual( + VMW_EXPECTED_DEFAULT_EVENTS, + ds.default_update_events[EventScope.NETWORK], + ) + self.assertSetEqual( + VMW_EXPECTED_DEFAULT_EVENTS, + ds.supported_update_events[EventScope.NETWORK], + ) + @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) @@ -602,6 +620,16 @@ def test_get_subplatform(self, m_which_fn, m_fn): ), ) + # Test to ensure that network is configured from metadata on each boot. + self.assertSetEqual( + VMW_EXPECTED_DEFAULT_EVENTS, + ds.default_update_events[EventScope.NETWORK], + ) + self.assertSetEqual( + VMW_EXPECTED_DEFAULT_EVENTS, + ds.supported_update_events[EventScope.NETWORK], + ) + @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") def test_get_data_metadata_with_vmware_rpctool(self, m_which_fn, m_fn): @@ -746,6 +774,81 @@ def setUp(self): self.datasource = DataSourceVMware.DataSourceVMware self.tdir = self.tmp_dir() + def test_get_subplatform(self): + paths = helpers.Paths({"cloud_dir": self.tdir}) + ds = self.datasource( + sys_cfg={"disable_vmware_customization": True}, + distro={}, + paths=paths, + ) + # Prepare the conf file + conf_file = self.tmp_path("test-cust", self.tdir) + conf_content = dedent( + """\ + [CLOUDINIT] + METADATA = test-meta + """ + ) + util.write_file(conf_file, conf_content) + # Prepare the meta data file + metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_content = dedent( + """\ + { + "instance-id": "cloud-vm", + "local-hostname": "my-host.domain.com", + "network": { + "version": 2, + "ethernets": { + "eths": { + "match": { + "name": "ens*" + }, + "dhcp4": true + } + } + } + } + """ + ) + util.write_file(metadata_file, metadata_content) + + with mock.patch( + MPATH + "guestcust_util.set_customization_status", + return_value=("msg", b""), + ): + result = wrap_and_call( + "cloudinit.sources.DataSourceVMware", + { + "dmi.read_dmi_data": "vmware", + "util.del_dir": True, + "guestcust_util.search_file": self.tdir, + "guestcust_util.wait_for_cust_cfg_file": conf_file, + "guestcust_util.get_imc_dir_path": self.tdir, + }, + ds._get_data, + ) + self.assertTrue(result) + + self.assertEqual( + ds.subplatform, + "%s (%s)" + % ( + DataSourceVMware.DATA_ACCESS_METHOD_IMC, + DataSourceVMware.get_imc_key_name("metadata"), + ), + ) + + # Test to ensure that network is configured from metadata on each boot. + self.assertSetEqual( + VMW_EXPECTED_DEFAULT_EVENTS, + ds.default_update_events[EventScope.NETWORK], + ) + self.assertSetEqual( + VMW_EXPECTED_DEFAULT_EVENTS, + ds.supported_update_events[EventScope.NETWORK], + ) + def test_get_data_false_on_none_dmi_data(self): """When dmi for system-product-name is None, get_data returns False.""" paths = helpers.Paths({"cloud_dir": self.tdir}) diff --git a/tests/unittests/test_upgrade.py b/tests/unittests/test_upgrade.py index 32a3d7c2ba3..fdf368d6f53 100644 --- a/tests/unittests/test_upgrade.py +++ b/tests/unittests/test_upgrade.py @@ -156,8 +156,10 @@ class TestUpgrade: "Vultr": {"netcfg"}, "VMware": { "data_access_method", + "default_update_events", "rpctool", "rpctool_fn", + "supported_update_events", }, "WSL": {"instance_name"}, }