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

Bug #1946493 reported by Andrew Kutz
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
cloud-init
Fix Released
Medium
Unassigned

Bug Description

This bug was first reported at https://github.com/kubernetes-sigs/image-builder/issues/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 https://github.com/canonical/cloud-init/commit/abd2da5777195e7e432b0d53a3f7f29d071dd50e, 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.

Revision history for this message
Andrew Kutz (akutz) wrote :

Filed a PR to address the bug at https://github.com/canonical/cloud-init/pull/1058

James Falcon (falcojr)
Changed in cloud-init:
status: New → Confirmed
importance: Undecided → Medium
James Falcon (falcojr)
Changed in cloud-init:
status: Confirmed → Fix Committed
Revision history for this message
James Falcon (falcojr) wrote : Fixed in cloud-init version 21.4.

This bug is believed to be fixed in cloud-init in version 21.4. If this is still a problem for you, please make a comment and set the state back to New

Thank you.

Changed in cloud-init:
status: Fix Committed → Fix Released
Revision history for this message
James Falcon (falcojr) wrote :
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.