Skip to content

Commit

Permalink
feat(vmware): Support network events
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
akutz committed Feb 28, 2025
1 parent 9704ba8 commit 5c6b94f
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 0 deletions.
33 changes: 33 additions & 0 deletions cloudinit/sources/DataSourceVMware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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)

Expand All @@ -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.
Expand All @@ -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,
Expand Down
103 changes: 103 additions & 0 deletions tests/unittests/sources/test_vmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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})
Expand Down
2 changes: 2 additions & 0 deletions tests/unittests/test_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ class TestUpgrade:
"Vultr": {"netcfg"},
"VMware": {
"data_access_method",
"default_update_events",
"rpctool",
"rpctool_fn",
"supported_update_events",
},
"WSL": {"instance_name"},
}
Expand Down

0 comments on commit 5c6b94f

Please sign in to comment.