Skip to content

Using "set-name" with interface specific DNS with a v2 network config causes a KeyError #3914

@ubuntu-server-builder

Description

@ubuntu-server-builder

This bug was originally filed in Launchpad as LP: #1946493

Launchpad details
affected_projects = []
assignee = None
assignee_name = None
date_closed = 2021-11-02T19:54:32.703386+00:00
date_created = 2021-10-08T14:22:27.818529+00:00
date_fix_committed = 2021-10-08T17:50:21.214952+00:00
date_fix_released = 2021-11-02T19:54:32.703386+00:00
id = 1946493
importance = medium
is_complete = True
lp_url = https://bugs.launchpad.net/cloud-init/+bug/1946493
milestone = None
owner = akutz
owner_name = Andrew Kutz
private = False
status = fix_released
submitter = akutz
submitter_name = Andrew Kutz
tags = []
duplicates = []

Launchpad user Andrew Kutz(akutz) wrote on 2021-10-08T14:22:27.818529+00:00

This bug was first reported at kubernetes-sigs/image-builder#712 and occurs when the v2 network configuration directive "set-name" is used in conjunction with interface specific DNS settings.

Cloud-Provider: VMware, but does not matter as this bug is distro and DS agnostic
Config: The metadata was set to the following:

instance-id: "wlan-1-md-0-775d8846bf-9bfrd"
local-hostname: "wlan-1-md-0-775d8846bf-9bfrd"
wait-on-network:
  ipv4: false
  ipv6: false
network:
  version: 2
  ethernets:
    id0:
      match:
        macaddress: "00:50:56:a1:d8:a7"
      set-name: "eth0"
      wakeonlan: true
      addresses:
      - "10.196.27.122/28"
      gateway4: "10.196.27.126"
      nameservers:
        addresses:
        - "10.102.102.132"
        - "10.102.102.133"
        - "10.90.24.1"
        search:
        - "refsa1.bn.schiff.telekom.de"

Again though, the platform is likely irrelevant as this seems to be a bug introduced with abd2da5, and can occur on any platform with and datasource as long as network v2 config is used with "set-name" and interface specific DNS.

The bug can be surfaced via unit test by patching the v21.3 version of Cloud-Init with the following:

diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py
index 84e8308a..c0aa78a0 100644
--- a/cloudinit/net/tests/test_network_state.py
+++ b/cloudinit/net/tests/test_network_state.py
@@ -52,6 +52,7 @@ network:
     eth1:
       match:
         macaddress: '66:77:88:99:00:11'
+      set-name: "eth2"
       nameservers:
         search: [foo.local, bar.local]
         addresses: [4.4.4.4]

Next, run the affected test from the root of the Cloud-Init source tree:

$ make clean_pyc && \
  PYTHONPATH="$(pwd)" \
  python3 -m pytest -v cloudinit/net/tests/test_network_state.py

The output will resemble the following:

====================================================================== test session starts ======================================================================
platform darwin -- Python 3.9.7, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/local/opt/python@3.9/bin/python3.9
cachedir: .pytest_cache
rootdir: /Users/akutz/Projects/cloud-init, configfile: tox.ini
collected 10 items                                                                                                                                              

cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfig::test_empty_v1_config_gets_network_state PASSED                                    [ 10%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfig::test_empty_v2_config_gets_network_state PASSED                                    [ 20%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfig::test_missing_version_returns_none PASSED                                          [ 30%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfig::test_unknown_versions_returns_none PASSED                                         [ 40%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfig::test_valid_config_gets_network_state PASSED                                       [ 50%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfig::test_version_2_passes_self_as_config PASSED                                       [ 60%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseConfigV2::test_version_2_ignores_renderer_key PASSED                                      [ 70%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseNameservers::test_v1_nameservers_valid PASSED                                             [ 80%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseNameservers::test_v1_nameservers_invalid PASSED                                           [ 90%]
cloudinit/net/tests/test_network_state.py::TestNetworkStateParseNameservers::test_v2_nameservers FAILED                                                   [100%]

=========================================================================== FAILURES ============================================================================
_____________________________________________________ TestNetworkStateParseNameservers.test_v2_nameservers ______________________________________________________

self = <cloudinit.net.tests.test_network_state.TestNetworkStateParseNameservers object at 0x10a7db9d0>

    def test_v2_nameservers(self):
>       config = self._parse_network_state_from_config(V2_CONFIG_NAMESERVERS)

cloudinit/net/tests/test_network_state.py:139: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cloudinit/net/tests/test_network_state.py:114: in _parse_network_state_from_config
    return network_state.parse_net_config_data(yaml['network'])
cloudinit/net/network_state.py:1074: in parse_net_config_data
    nsi.parse_config(skip_broken=skip_broken)
cloudinit/net/network_state.py:261: in parse_config
    self.parse_config_v2(skip_broken=skip_broken)
cloudinit/net/network_state.py:310: in parse_config_v2
    self._v2_common(command)
cloudinit/net/network_state.py:722: in _v2_common
    self._handle_individual_nameserver(name_cmd, iface)
cloudinit/net/network_state.py:91: in decorator
    return func(self, command, *args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <cloudinit.net.network_state.NetworkStateInterpreter object at 0x10a7e4f10>
command = {'address': ['4.4.4.4'], 'search': ['foo.local', 'bar.local'], 'type': 'nameserver'}, iface = 'eth1'

    @ensure_command_keys(['address'])
    def _handle_individual_nameserver(self, command, iface):
        _iface = self._network_state.get('interfaces')
        nameservers, search = self._parse_dns(command)
>       _iface[iface]['dns'] = {'nameservers': nameservers, 'search': search}
E       KeyError: 'eth1'

cloudinit/net/network_state.py:546: KeyError
----------------------------------------------------------------------- Captured log call -----------------------------------------------------------------------
2021-10-08 00:57:41 DEBUG     cloudinit.net.network_state:network_state.py:670 v2(ethernets) -> v1(physical):
{'type': 'physical', 'name': 'eth0', 'mac_address': '00:11:22:33:44:55', 'match': {'macaddress': '00:11:22:33:44:55'}}
2021-10-08 00:57:41 DEBUG     cloudinit.net.network_state:network_state.py:670 v2(ethernets) -> v1(physical):
{'type': 'physical', 'name': 'eth2', 'mac_address': '66:77:88:99:00:11', 'match': {'macaddress': '66:77:88:99:00:11'}}
2021-10-08 00:57:41 DEBUG     cloudinit.net.network_state:network_state.py:711 v2_common: handling config:
{'eth0': {'match': {'macaddress': '00:11:22:33:44:55'}, 'nameservers': {'search': ['spam.local', 'eggs.local'], 'addresses': ['8.8.8.8']}}, 'eth1': {'match': {'macaddress': '66:77:88:99:00:11'}, 'set-name': 'eth2', 'nameservers': {'search': ['foo.local', 'bar.local'], 'addresses': ['4.4.4.4']}}}
======================================================================= warnings summary ========================================================================
../../../../usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1183
  /usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1183: PytestDeprecationWarning: The --strict option is deprecated, use --strict-markers instead.
    self.issue_config_time_warning(

conftest.py:68
  /Users/akutz/Projects/cloud-init/conftest.py:68: PytestDeprecationWarning: @pytest.yield_fixture is deprecated.
  Use @pytest.fixture instead; they are the same.
    @pytest.yield_fixture(autouse=True)

conftest.py:169
  /Users/akutz/Projects/cloud-init/conftest.py:169: PytestDeprecationWarning: @pytest.yield_fixture is deprecated.
  Use @pytest.fixture instead; they are the same.
    def httpretty():

-- Docs: https://docs.pytest.org/en/stable/warnings.html
==================================================================== short test summary info ====================================================================
FAILED cloudinit/net/tests/test_network_state.py::TestNetworkStateParseNameservers::test_v2_nameservers - KeyError: 'eth1'
============================================================ 1 failed, 9 passed, 3 warnings in 0.60s ============================================================

This is is occurring because the code to iterate over the interfaces when configuring interface-specific DNS is using the original interface name, not the one from the "set-name" directive. The following patch corrects the issue:

diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 95b064f0..06ff8e96 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -543,7 +543,11 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
     def _handle_individual_nameserver(self, command, iface):
         _iface = self._network_state.get('interfaces')
         nameservers, search = self._parse_dns(command)
-        _iface[iface]['dns'] = {'nameservers': nameservers, 'search': search}
+        try:
+            _iface[iface]['dns'] = {'nameservers': nameservers, 'search': search}
+        except:
+            print("original iface name: %s\niface dict: %s\n" % (iface, _iface))
+            raise
 
     @ensure_command_keys(['destination'])
     def handle_route(self, command):
diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py
index 84e8308a..45e99171 100644
--- a/cloudinit/net/tests/test_network_state.py
+++ b/cloudinit/net/tests/test_network_state.py
@@ -52,6 +52,7 @@ network:
     eth1:
       match:
         macaddress: '66:77:88:99:00:11'
+      set-name: "ens92"
       nameservers:
         search: [foo.local, bar.local]
         addresses: [4.4.4.4]

Now the above test passes. A PR will be opened on Cloud-Init's GitHub repository with the above patch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    launchpadMigrated from Launchpad

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions