Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions cloudinit/sources/DataSourceAzure.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ def get_resource_disk_on_freebsd(port_id) -> Optional[str]:
"disk_aliases": {"ephemeral0": RESOURCE_DISK_PATH},
"apply_network_config": True, # Use IMDS published network configuration
"apply_network_config_for_secondary_ips": True, # Configure secondary ips
"apply_network_config_set_name": True, # Use set-name for NICs
"experimental_skip_ready_report": False, # Skip final ready report
}

Expand Down Expand Up @@ -362,6 +363,10 @@ def _unpickle(self, ci_pkl_version: int) -> None:
self._system_uuid = None
self._vm_id = None
self._wireserver_endpoint = DEFAULT_WIRESERVER_ENDPOINT
self.ds_cfg.setdefault(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage of unpickle case across upgrade path && system reboot.

  1. While we are here, shouldn't we also be setting apply_network_config_for_secondary_ips within _unpickle too to ensure those defaults under which the prior instance launch are preserved in the unpickled datasource?

  2. I think we also have a preexisting test coverage gap which should have alerted us to this potential _unpickle initialization issue w.r.t. apply_network_config_for_secondary_ips

--- a/tests/unittests/test_upgrade.py
+++ b/tests/unittests/test_upgrade.py
@@ -294,6 +294,8 @@ class TestUpgrade:
         missing_attrs = ds.__dict__.keys() - previous_obj_pkl.__dict__.keys()
         for attr in missing_attrs:
             assert attr in expected
+        missing_ds_cfg_attrs = ds.ds_cfg.keys() - previous_obj_pkl.ds_cfg.keys()
+        assert set() == missing_ds_cfg_attrs
 
     def test_networking_set_on_distro(self, previous_obj_pkl):
         """We always expect to have ``.networking`` on ``Distro`` objects."""

This test change above also found another disparity with experimental_skip_ready_report too.

"apply_network_config_set_name",
BUILTIN_DS_CONFIG["apply_network_config_set_name"],
)

def __str__(self):
root = sources.DataSource.__str__(self)
Expand Down Expand Up @@ -1619,6 +1624,9 @@ def _generate_network_config(self):
apply_network_config_for_secondary_ips=self.ds_cfg.get(
"apply_network_config_for_secondary_ips"
),
apply_network_config_set_name=self.ds_cfg.get(
"apply_network_config_set_name"
),
Comment on lines 1624 to +1629
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that _unpickle and __init__ should properly set both apply_network_config_for_secondary_ips and apply_network_config_set_name values to defaults (or overrides in system config files), do we need to use ds_cfg.get or can we instead just use the return the expected value at the key index with self.ds_cfg["apply_network_config_set_name"]?

)
except Exception as e:
LOG.error(
Expand Down Expand Up @@ -2095,6 +2103,7 @@ def generate_network_config_from_instance_network_metadata(
network_metadata: dict,
*,
apply_network_config_for_secondary_ips: bool,
apply_network_config_set_name: bool = True,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a default value on this mandatory param as we are setting it in both __init__ and _unpickle.

Suggested change
apply_network_config_set_name: bool = True,
apply_network_config_set_name: bool,

) -> dict:
"""Convert imds network metadata dictionary to network v2 configuration.

Expand All @@ -2108,7 +2117,11 @@ def generate_network_config_from_instance_network_metadata(
# First IPv4 and/or IPv6 address will be obtained via DHCP.
# Any additional IPs of each type will be set as static
# addresses.
nicname = "eth{idx}".format(idx=idx)
mac = normalize_mac_address(intf["macAddress"])
if apply_network_config_set_name:
nicname = "eth{idx}".format(idx=idx)
else:
nicname = "enx{mac}".format(mac=mac.replace(":", ""))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm predictable interface naming convention prefix aligns with predictable naming expectations in systemd

Comment on lines +2120 to +2124
dhcp_override = {"route-metric": (idx + 1) * 100}
# DNS resolution through secondary NICs is not supported, disable it.
if idx > 0:
Expand Down Expand Up @@ -2152,10 +2165,9 @@ def generate_network_config_from_instance_network_metadata(
"{ip}/{prefix}".format(ip=privateIp, prefix=netPrefix)
)
if dev_config and has_ip_address:
mac = normalize_mac_address(intf["macAddress"])
dev_config.update(
{"match": {"macaddress": mac.lower()}, "set-name": nicname}
)
dev_config["match"] = {"macaddress": mac.lower()}
if apply_network_config_set_name:
dev_config["set-name"] = nicname
driver = determine_device_driver_for_mac(mac)
if driver:
dev_config["match"]["driver"] = driver
Expand Down
17 changes: 17 additions & 0 deletions doc/rtd/reference/datasources/azure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ The settings that may be configured are:

Boolean to configure secondary IP address(es) for each NIC per IMDS
configuration. Default is True.

* :command:`apply_network_config_set_name`

Boolean to include ``set-name`` directives in the generated network
configuration, which renames interfaces to ``ethX`` naming. When set to
False, interfaces are matched by MAC address (and optionally driver)
without renaming, and retain kernel-assigned names. Default is True.

Azure's IMDS does not guarantee the ordering of NICs in the network metadata
response (see `Azure IMDS documentation
<https://learn.microsoft.com/azure/virtual-machines/instance-metadata-service>`_).
Because cloud-init derives ``ethX`` names from the IMDS response order,
NIC names may change between reboots. Disabling this option avoids that
problem by matching interfaces on MAC address (and optionally driver),
allowing the kernel or udev to assign and retain stable names.

* :command:`data_dir`

Path used to read meta-data files and write crawled data.
Expand All @@ -67,6 +83,7 @@ An example configuration with the default values is provided below:
Azure:
apply_network_config: true
apply_network_config_for_secondary_ips: true
apply_network_config_set_name: false
data_dir: /var/lib/waagent
disk_aliases:
ephemeral0: /dev/disk/cloud/azure_resource
Expand Down
138 changes: 123 additions & 15 deletions tests/unittests/sources/test_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,91 @@ def test_parsing_scenarios(
== expected
)

@pytest.mark.parametrize(
"set_name,expected",
[
(
True,
{
"ethernets": {
"eth0": {
"dhcp4": True,
"dhcp4-overrides": {"route-metric": 100},
"dhcp6": True,
"dhcp6-overrides": {"route-metric": 100},
"match": {"macaddress": "00:0d:3a:04:75:98"},
"set-name": "eth0",
},
"eth1": {
"dhcp4": True,
"dhcp4-overrides": {
"route-metric": 200,
"use-dns": False,
},
"dhcp6": False,
"match": {"macaddress": "22:0d:3a:04:75:98"},
"set-name": "eth1",
},
},
"version": 2,
},
),
(
False,
{
"ethernets": {
"enx000d3a047598": {
"dhcp4": True,
"dhcp4-overrides": {"route-metric": 100},
"dhcp6": True,
"dhcp6-overrides": {"route-metric": 100},
"match": {"macaddress": "00:0d:3a:04:75:98"},
},
"enx220d3a047598": {
"dhcp4": True,
"dhcp4-overrides": {
"route-metric": 200,
"use-dns": False,
},
"dhcp6": False,
"match": {"macaddress": "22:0d:3a:04:75:98"},
},
},
"version": 2,
},
),
],
)
def test_set_name_config(self, mock_get_interfaces, set_name, expected):
"""Verify set-name with two NICs (primary with IPv6, secondary)."""
two_nic_metadata = {
"interface": [
{
"macAddress": "000D3A047598",
"ipv6": {
"subnet": [{"prefix": "64", "address": "fd00::"}],
"ipAddress": [{"privateIpAddress": "fd00::4"}],
},
"ipv4": {
"subnet": [{"prefix": "24", "address": "10.0.0.0"}],
"ipAddress": [
{
"privateIpAddress": "10.0.0.4",
"publicIpAddress": "104.46.124.81",
}
],
},
},
SECONDARY_INTERFACE,
]
}
result = dsaz.generate_network_config_from_instance_network_metadata(
two_nic_metadata,
apply_network_config_for_secondary_ips=True,
apply_network_config_set_name=set_name,
)
assert result == expected


class TestNetworkConfig:
fallback_config = {
Expand All @@ -1004,22 +1089,45 @@ class TestNetworkConfig:
],
}

def test_single_ipv4_nic_configuration(
self, azure_ds, mock_get_interfaces
):
"""Network config emits dhcp on single nic with ipv4"""
expected = {
"ethernets": {
"eth0": {
"dhcp4": True,
"dhcp4-overrides": {"route-metric": 100},
"dhcp6": False,
"match": {"macaddress": "00:0d:3a:04:75:98"},
"set-name": "eth0",
@pytest.mark.parametrize(
"set_name,expected",
[
(
True,
{
"ethernets": {
"eth0": {
"dhcp4": True,
"dhcp4-overrides": {"route-metric": 100},
"dhcp6": False,
"match": {"macaddress": "00:0d:3a:04:75:98"},
"set-name": "eth0",
},
},
"version": 2,
},
},
"version": 2,
}
),
(
False,
{
"ethernets": {
"enx000d3a047598": {
"dhcp4": True,
"dhcp4-overrides": {"route-metric": 100},
"dhcp6": False,
"match": {"macaddress": "00:0d:3a:04:75:98"},
},
},
"version": 2,
},
),
],
)
def test_network_config(
self, azure_ds, mock_get_interfaces, set_name, expected
):
"""Verify network_config via ds_cfg for set-name enabled/disabled."""
azure_ds.ds_cfg["apply_network_config_set_name"] = set_name
azure_ds._metadata_imds = NETWORK_METADATA

assert azure_ds.network_config == expected
Expand Down
Loading