Repository: canonical/cloud-init (branch: holman/dhcpcd)
Commit date: 2024-01-12T20:05:57Z

Workflow file: .github/workflows/check_format.yml (Lint Tests)
name: Lint Tests
on:
  pull_request:
  push:
    branches:
      - main

concurrency:
  group: 'ci-${{ github.workflow }}-${{ github.ref }}'
  cancel-in-progress: true
defaults:
  run:
    shell: sh -ex {0}

jobs:
  check_format:
    strategy:
      fail-fast: false
      matrix:
        env: [ruff, mypy, pylint, black, isort]
        lint-with:
          - {tip-versions: false, os: ubuntu-20.04}
          - {tip-versions: true, os: ubuntu-latest}
    name: Check ${{ matrix.lint-with.tip-versions && 'tip-' || '' }}${{ matrix.env }}
    runs-on: ${{ matrix.lint-with.os }}
    services:
      pypi_wayback:
        image: ghcr.io/waleko/pypi-wayback
        ports:
          - 8629:8080
    steps:
      - name: "Checkout #1"
        uses: actions/checkout@v3.0.0

      - name: "Checkout #2 (for tools/read-version)"
        run: |
          git fetch --unshallow
          git remote add upstream https://git.launchpad.net/cloud-init

      - name: Dependencies
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get -qy update
          sudo DEBIAN_FRONTEND=noninteractive apt-get -qy install tox

      - name: Print version
        run: python3 --version

      - name: Test
        if: ${{ !matrix.lint-with.tip-versions }}
        env:
          # matrix env: not to be confused w/environment variables or testenv
          TOXENV: ${{ matrix.env }}
        run: tox
      - name: Test (tip versions)
        if: matrix.lint-with.tip-versions
        continue-on-error: true
        env:
          TOXENV: tip-${{ matrix.env }}
        run: tox
  schema-format:
    strategy:
      fail-fast: false
    name: Check json format
    runs-on: ubuntu-22.04
    services:
      pypi_wayback:
        image: ghcr.io/waleko/pypi-wayback
        ports:
          - 8629:8080
    steps:
      - name: "Checkout #1"
        uses: actions/checkout@v3.0.0

      - name: "Test format"
        run: |
          tools/check_json_format.sh cloudinit/config/schemas/schema-cloud-config-v1.json
          tools/check_json_format.sh cloudinit/config/schemas/schema-network-config-v1.json
          tools/check_json_format.sh cloudinit/config/schemas/versions.schema.cloud-config.json

  doc:
    strategy:
      fail-fast: false
    name: Check docs
    runs-on: ubuntu-22.04
    services:
      pypi_wayback:
        image: ghcr.io/waleko/pypi-wayback
        ports:
          - 8629:8080
    steps:
      - name: "Checkout #1"
        uses: actions/checkout@v3.0.0

      - name: "Checkout #2 (for tools/read-version)"
        run: |
          git fetch --unshallow
          git remote add upstream https://git.launchpad.net/cloud-init
      - name: "Install Python 3.10"
        uses: actions/setup-python@v4
        with:
          python-version: '3.10.8'
      - name: "Install dependencies"
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get -qy update
          sudo DEBIAN_FRONTEND=noninteractive apt-get -qy install tox
      - name: "Spellcheck"
        run: |
          tox
        env:
          TOXENV: doc-spelling
      - name: "Build docs"
        env:
          TOXENV: doc
        run: |
          tox

  shell-lint:
    name: Shell Lint
    runs-on: ubuntu-22.04
    services:
      pypi_wayback:
        image: ghcr.io/waleko/pypi-wayback
        ports:
          - 8629:8080
    steps:
      - name: Checkout
        uses: actions/checkout@v3.0.0

      - name: Install ShellCheck
        run: |
          sudo apt-get update
          sudo apt install shellcheck

      - name: Run ShellCheck
        run: |
          shellcheck ./tools/ds-identify

  check-cla-signers:
    runs-on: ubuntu-22.04
    services:
      pypi_wayback:
        image: ghcr.io/waleko/pypi-wayback
        ports:
          - 8629:8080
    steps:
      - uses: actions/checkout@v3.0.0

      - name: Check CLA signers file
        run: tools/check-cla-signers

env:
  PIP_INDEX_URL: http://localhost:8629/2024-01-12
  UV_INDEX_URL: http://localhost:8629/2024-01-12


Failed build logs:
step_name: Check pylint/6_Test.txt
log: ##[group]Run tox
[36;1mtox[0m
shell: /usr/bin/sh -ex {0}
env:
  TOXENV: pylint
##[endgroup]
+ tox
GLOB sdist-make: /home/runner/work/cloud-init/cloud-init/setup.py
pylint create: /home/runner/work/cloud-init/cloud-init/.tox/pylint
pylint installdeps: pylint===2.13.9, -r/home/runner/work/cloud-init/cloud-init/test-requirements.txt, -r/home/runner/work/cloud-init/cloud-init/integration-requirements.txt
pylint inst: /home/runner/work/cloud-init/cloud-init/.tox/.tmp/package/1/cloud-init-23.4+88.g0e8fe220.zip
pylint installed: adal==1.2.7,anyio==4.2.0,appdirs==1.4.4,applicationinsights==0.11.10,argcomplete==3.1.6,astroid==2.11.7,attrs==23.2.0,autopage==0.5.2,azure-cli-core==2.56.0,azure-cli-telemetry==1.1.0,azure-common==1.1.28,azure-core==1.29.6,azure-identity==1.15.0,azure-mgmt-compute==30.4.0,azure-mgmt-core==1.4.0,azure-mgmt-network==25.2.0,azure-mgmt-resource==23.0.1,bcrypt==4.1.2,boto3==1.34.17,botocore==1.34.17,cachetools==5.3.2,certifi==2023.11.17,cffi==1.16.0,charset-normalizer==3.3.2,circuitbreaker==1.4.0,cliff==4.5.0,cloud-init==23.4+88.g0e8fe220,cmd2==2.4.3,configobj==5.0.8,coverage==7.4.0,cryptography==41.0.7,debtcollector==2.5.0,decorator==5.1.1,dill==0.3.7,distro==1.9.0,dogpile.cache==1.3.0,exceptiongroup==1.2.0,google-api-core==2.15.0,google-api-python-client==2.113.0,google-auth==2.26.2,google-auth-httplib2==0.2.0,googleapis-common-protos==1.62.0,httplib2==0.22.0,humanfriendly==10.0,ibm-cloud-sdk-core==3.18.2,ibm-platform-services==0.49.0,ibm-vpc==0.20.0,idna==3.6,importlib-metadata==7.0.1,importlib-resources==6.1.1,iniconfig==2.0.0,iso8601==2.1.0,isodate==0.6.1,isort==5.13.2,Jinja2==3.1.3,jmespath==1.0.1,jsonpatch==1.33,jsonpointer==2.4,jsonschema==4.20.0,jsonschema-specifications==2023.12.1,keystoneauth1==5.5.0,knack==0.11.0,lazy-object-proxy==1.10.0,MarkupSafe==2.1.3,mccabe==0.7.0,msal==1.24.0b2,msal-extensions==1.0.0,msgpack==1.0.7,msrest==0.7.1,msrestazure==0.6.4,netaddr==0.10.1,netifaces==0.11.0,oauthlib==3.2.2,oci==2.118.2,openstacksdk==1.4.0,os-service-types==1.7.0,osc-lib==3.0.0,oslo.config==9.3.0,oslo.i18n==6.2.0,oslo.serialization==5.3.0,oslo.utils==6.3.0,packaging==23.2,paramiko==3.4.0,passlib==1.7.4,pbr==6.0.0,pkginfo==1.9.6,pkgutil-resolve-name==1.3.10,platformdirs==4.1.0,pluggy==1.3.0,portalocker==2.8.2,prettytable==3.9.0,protobuf==3.19.6,psutil==5.9.7,pyasn1==0.5.1,pyasn1-modules==0.3.0,pycloudlib==1!5.12.1,pycparser==2.21,pygments==2.17.2,PyJWT==2.8.0,pylint==2.13.9,PyNaCl==1.5.0,pyOpenSSL==23.3.0,pyparsing==2.4.7,pyperclip==1.8.2,pyserial==3.5,pytest==7.4.4,pytest-cov==4.1.0,pytest-mock==3.12.0,python-cinderclient==9.4.0,python-dateutil==2.8.2,python-keystoneclient==5.3.0,python-novaclient==18.4.0,python-openstackclient==6.4.0,pytz==2023.3.post1,PyYAML==6.0.1,qemu.qmp==0.0.3,referencing==0.32.1,requests==2.31.0,requests-oauthlib==1.3.1,requestsexceptions==1.4.0,responses==0.24.1,rfc3986==2.0.0,rpds-py==0.17.1,rsa==4.9,s3transfer==0.10.0,simplejson==3.19.2,six==1.16.0,sniffio==1.3.0,stevedore==5.1.0,tabulate==0.9.0,toml==0.10.0,tomli==2.0.1,typing-extensions==4.9.0,tzdata==2023.4,uritemplate==4.1.1,urllib3==2.1.0,wcwidth==0.2.13,wrapt==1.16.0,zipp==3.17.0
pylint run-test-pre: PYTHONHASHSEED='1345909868'
pylint run-test: commands[0] | /home/runner/work/cloud-init/cloud-init/.tox/pylint/bin/python -m pylint cloudinit/ tests/ tools/ conftest.py setup.py
************* Module cloudinit.net.dhcp
cloudinit/net/dhcp.py:538: [W0235(useless-super-delegation), Dhcpcd.__init__] Useless super delegation in method '__init__'

------------------------------------
Your code has been rated at 10.00/10

ERROR: InvocationError for command /home/runner/work/cloud-init/cloud-init/.tox/pylint/bin/python -m pylint cloudinit/ tests/ tools/ conftest.py setup.py (exited with code 4)
___________________________________ summary ____________________________________
ERROR:   pylint: commands failed
##[error]Process completed with exit code 1.


Changed files:
cloudinit/distros/__init__.py
cloudinit/net/dhcp.py
cloudinit/sources/__init__.py
tests/unittests/net/test_dhcp.py
tests/unittests/sources/test_cloudstack.py
tests/unittests/sources/test_ec2.py
tests/unittests/sources/test_init.py
tests/unittests/sources/test_openstack.py

Fix diff:
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 5c891f26e..c02166ad2 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -170,6 +170,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
         self._runner = helpers.Runners(paths)
         self.package_managers: List[PackageManager] = []
         self._dhcp_client = None
+        self._fallback_interface = None
 
     def _unpickle(self, ci_pkl_version: int) -> None:
         """Perform deserialization fixes for Distro."""
@@ -1274,6 +1275,13 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
             "/bin/true",
         ] + (["-cf", config_file, interface] if config_file else [interface])
 
+    @property
+    def fallback_interface(self):
+        """Determine the network interface used during local network config."""
+        if self._fallback_interface is None:
+            self._fallback_interface = net.find_fallback_nic()
+        return self._fallback_interface
+
 
 def _apply_hostname_transformations_to_url(url: str, transformations: list):
     """
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 9c94b3f92..7b2d41df1 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -19,8 +19,6 @@ import configobj
 
 from cloudinit import subp, temp_utils, util
 from cloudinit.net import (
-    find_fallback_nic,
-    get_devicelist,
     get_ib_interface_hwaddr,
     get_interface_mac,
     is_ib_interface,
@@ -98,17 +96,8 @@ def maybe_perform_dhcp_discovery(distro, nic=None, dhcp_log_func=None):
         from the dhclient discovery if run, otherwise an empty list is
         returned.
     """
-    if nic is None:
-        nic = find_fallback_nic()
-        if nic is None:
-            LOG.debug("Skip dhcp_discovery: Unable to find fallback nic.")
-            raise NoDHCPLeaseInterfaceError()
-    elif nic not in get_devicelist():
-        LOG.debug(
-            "Skip dhcp_discovery: nic %s not found in get_devicelist.", nic
-        )
-        raise NoDHCPLeaseInterfaceError()
-    return distro.dhcp_client.dhcp_discovery(nic, dhcp_log_func, distro)
+    interface = nic or distro.fallback_interface
+    return distro.dhcp_client.dhcp_discovery(interface, dhcp_log_func, distro)
 
 
 def networkd_parse_lease(content):
@@ -185,7 +174,7 @@ class DhcpClient(abc.ABC):
     def parse_static_routes(routes: str) -> List[Tuple[str, str]]:
         return []
 
-    def get_newest_lease(self, distro) -> Dict[str, Any]:
+    def get_newest_lease(self, interface: str) -> Dict[str, Any]:
         return {}
 
 
@@ -218,15 +207,15 @@ class IscDhclient(DhcpClient):
             dhcp_leases.append(dict(lease_options))
         return dhcp_leases
 
-    def get_newest_lease(self, distro) -> Dict[str, Any]:
+    def get_newest_lease(self, interface: str) -> Dict[str, Any]:
         """Get the most recent lease from the ephemeral phase as a dict.
 
         Return a dict of dhcp options. The dict contains key value
         pairs from the most recent lease.
 
-        @param distro: a distro object - not used in this class, but required
-                       for function signature compatibility with other classes
-                       that require a distro object
+        @param interface: an interface string - not used in this class, but
+            required for function signature compatibility with other classes
+            that require a distro object
         @raises: InvalidDHCPLeaseFileError on empty or unparseable leasefile
             content.
         """
@@ -355,7 +344,7 @@ class IscDhclient(DhcpClient):
             )
         if dhcp_log_func is not None:
             dhcp_log_func(out, err)
-        lease = self.get_newest_lease(distro)
+        lease = self.get_newest_lease(interface)
         if lease:
             return lease
         raise InvalidDHCPLeaseFileError()
@@ -535,9 +524,6 @@ class IscDhclient(DhcpClient):
 class Dhcpcd(DhcpClient):
     client_name = "dhcpcd"
 
-    def __init__(self):
-        super().__init__()
-
     def dhcp_discovery(
         self,
         interface: str,
@@ -547,12 +533,12 @@ class Dhcpcd(DhcpClient):
         """Run dhcpcd on the interface without scripts/filesystem artifacts.
 
         @param interface: Name of the network interface on which to send a
-                          dhcp request
+            dhcp request
         @param dhcp_log_func: A callable accepting the client output and
-                              error streams.
+            error streams.
         @param distro: a distro object for network interface manipulation
         @return: dict of lease options representing the most recent dhcp lease
-                 parsed from the dhclient.lease file
+            parsed from the dhclient.lease file
         """
         LOG.debug("Performing a dhcp discovery on %s", interface)
 
@@ -580,7 +566,7 @@ class Dhcpcd(DhcpClient):
             )
             if dhcp_log_func is not None:
                 dhcp_log_func(out, err)
-            lease = self.get_newest_lease(distro)
+            lease = self.get_newest_lease(interface)
             if lease:
                 return lease
             raise NoDHCPLeaseError("No lease found")
@@ -644,12 +630,10 @@ class Dhcpcd(DhcpClient):
         # for compatibility we still expect a list of leases
         return lease
 
-    def get_newest_lease(self, distro) -> Dict[str, Any]:
-        """Return a lease
-
-        Return a list of dicts of dhcp options. Each dict contains key value
-        pairs a specific lease in order from oldest to newest.
+    def get_newest_lease(self, interface: str) -> Dict[str, Any]:
+        """Return a dict of dhcp options.
 
+        @param interface: which interface to dump the lease from
         @raises: InvalidDHCPLeaseFileError on empty or unparseable leasefile
             content.
         """
@@ -659,11 +643,11 @@ class Dhcpcd(DhcpClient):
                     [
                         "dhcpcd",
                         "--dumplease",
-                        distro.fallback_interface,
+                        interface,
                     ],
                     rcs=[0, 1],
                 ).stdout,
-                distro.fallback_interface,
+                interface,
             )
 
         except subp.ProcessExecutionError as error:
@@ -781,17 +765,17 @@ class Udhcpc(DhcpClient):
         if dhcp_log_func is not None:
             dhcp_log_func(out, err)
 
-        return self.get_newest_lease(distro)
+        return self.get_newest_lease(interface)
 
-    def get_newest_lease(self, distro) -> Dict[str, Any]:
+    def get_newest_lease(self, interface: str) -> Dict[str, Any]:
         """Get the most recent lease from the ephemeral phase as a dict.
 
         Return a dict of dhcp options. The dict contains key value
         pairs from the most recent lease.
 
-        @param distro: a distro object - not used in this class, but required
-                       for function signature compatibility with other classes
-                       that require a distro object
+        @param interface: an interface name - not used in this class, but
+            required for function signature compatibility with other classes
+            that require a distro object
         @raises: InvalidDHCPLeaseFileError on empty or unparseable leasefile
             content.
         """
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index c207b5ed6..f9650268a 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -610,13 +610,11 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta):
     @property
     def fallback_interface(self):
         """Determine the network interface used during local network config."""
-        if self._fallback_interface is None:
-            self._fallback_interface = net.find_fallback_nic()
-            if self._fallback_interface is None:
-                LOG.warning(
-                    "Did not find a fallback interface on %s.", self.cloud_name
-                )
-        return self._fallback_interface
+        if self.distro.fallback_interface is None:
+            LOG.warning(
+                "Did not find a fallback interface on %s.", self.cloud_name
+            )
+        return self.distro.fallback_interface
 
     @property
     def platform_type(self):
diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py
index ffa3eab17..9af5f93aa 100644
--- a/tests/unittests/net/test_dhcp.py
+++ b/tests/unittests/net/test_dhcp.py
@@ -372,20 +372,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
     with_logs = True
     ib_address_prefix = "00:00:00:00:00:00:00:00:00:00:00:00"
 
-    @mock.patch("cloudinit.net.dhcp.find_fallback_nic")
-    def test_no_fallback_nic_found(self, m_fallback_nic):
-        """Log and do nothing when nic is absent and no fallback is found."""
-        m_fallback_nic.return_value = None  # No fallback nic found
-
-        with pytest.raises(NoDHCPLeaseInterfaceError):
-            maybe_perform_dhcp_discovery(MockDistro())
-
-        self.assertIn(
-            "Skip dhcp_discovery: Unable to find fallback nic.",
-            self.logs.getvalue(),
-        )
-
-    @mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
+    @mock.patch("cloudinit.distros.net.find_fallback_nic", return_value="eth9")
     @mock.patch("cloudinit.net.dhcp.os.remove")
     @mock.patch("cloudinit.net.dhcp.subp.subp")
     @mock.patch("cloudinit.net.dhcp.subp.which")
@@ -406,7 +393,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
             self.logs.getvalue(),
         )
 
-    @mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
+    @mock.patch("cloudinit.distros.net.find_fallback_nic", return_value="eth9")
     @mock.patch("cloudinit.net.dhcp.os.remove")
     @mock.patch("cloudinit.net.dhcp.subp.subp")
     @mock.patch("cloudinit.net.dhcp.subp.which")
@@ -435,17 +422,6 @@ class TestDHCPDiscoveryClean(CiTestCase):
             self.logs.getvalue(),
         )
 
-    @mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value=None)
-    def test_provided_nic_does_not_exist(self, m_fallback_nic):
-        """When the provided nic doesn't exist, log a message and no-op."""
-        with pytest.raises(NoDHCPLeaseInterfaceError):
-            maybe_perform_dhcp_discovery(MockDistro(), "idontexist")
-
-        self.assertIn(
-            "Skip dhcp_discovery: nic idontexist not found in get_devicelist.",
-            self.logs.getvalue(),
-        )
-
     @mock.patch("cloudinit.net.dhcp.subp.which")
     @mock.patch("cloudinit.net.dhcp.find_fallback_nic")
     def test_absent_dhclient_command(self, m_fallback, m_which):
diff --git a/tests/unittests/sources/test_cloudstack.py b/tests/unittests/sources/test_cloudstack.py
index 8ee04b3e1..b33c5e492 100644
--- a/tests/unittests/sources/test_cloudstack.py
+++ b/tests/unittests/sources/test_cloudstack.py
@@ -244,6 +244,13 @@ class TestCloudStackHostname(CiTestCase):
             )
         )
 
+        self.patches.enter_context(
+            mock.patch(
+                "cloudinit.distros.net.find_fallback_nic",
+                return_value="eth0",
+            )
+        )
+
         self.patches.enter_context(
             mock.patch(
                 MOD_PATH
@@ -304,7 +311,7 @@ class TestCloudStackHostname(CiTestCase):
         )
 
         ds = DataSourceCloudStack(
-            {}, MockDistro(), helpers.Paths({"run_dir": self.tmp})
+            {}, ubuntu.Distro("", {}, {}), helpers.Paths({"run_dir": self.tmp})
         )
         ds._fallback_interface = "eth0"
         with mock.patch(MOD_PATH + ".util.load_file"):
diff --git a/tests/unittests/sources/test_ec2.py b/tests/unittests/sources/test_ec2.py
index 5f60ad737..32fed89da 100644
--- a/tests/unittests/sources/test_ec2.py
+++ b/tests/unittests/sources/test_ec2.py
@@ -10,6 +10,7 @@ import requests
 import responses
 
 from cloudinit import helpers
+from cloudinit.distros import ubuntu
 from cloudinit.sources import DataSourceEc2 as ec2
 from tests.unittests import helpers as test_helpers
 
@@ -342,9 +343,11 @@ class TestEc2(test_helpers.ResponsesTestCase):
         p.start()
         self.addCleanup(p.stop)
 
-    def _setup_ds(self, sys_cfg, platform_data, md, md_version=None):
+    def _setup_ds(
+        self, sys_cfg, platform_data, md, md_version=None, distro=None
+    ):
         self.uris = []
-        distro = mock.MagicMock()
+        distro = distro or mock.MagicMock()
         distro.get_tmp_exec_path = self.tmp_dir
         paths = helpers.Paths({"run_dir": self.tmp})
         if sys_cfg is None:
@@ -846,7 +849,7 @@ class TestEc2(test_helpers.ResponsesTestCase):
 
     @mock.patch("cloudinit.net.ephemeral.EphemeralIPv6Network")
     @mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network")
-    @mock.patch("cloudinit.net.find_fallback_nic")
+    @mock.patch("cloudinit.distros.net.find_fallback_nic")
     @mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
     @mock.patch("cloudinit.sources.DataSourceEc2.util.is_FreeBSD")
     def test_ec2_local_performs_dhcp_on_non_bsd(
@@ -873,6 +876,7 @@ class TestEc2(test_helpers.ResponsesTestCase):
             platform_data=self.valid_platform_data,
             sys_cfg={"datasource": {"Ec2": {"strict_id": False}}},
             md={"md": DEFAULT_METADATA},
+            distro=ubuntu.Distro("", {}, {}),
         )
 
         ret = ds.get_data()
diff --git a/tests/unittests/sources/test_init.py b/tests/unittests/sources/test_init.py
index 44d63b816..b3764945d 100644
--- a/tests/unittests/sources/test_init.py
+++ b/tests/unittests/sources/test_init.py
@@ -6,6 +6,7 @@ import os
 import stat
 
 from cloudinit import importer, util
+from cloudinit.distros import ubuntu
 from cloudinit.event import EventScope, EventType
 from cloudinit.helpers import Paths
 from cloudinit.sources import (
@@ -73,7 +74,7 @@ class TestDataSource(CiTestCase):
     def setUp(self):
         super(TestDataSource, self).setUp()
         self.sys_cfg = {"datasource": {"_undef": {"key1": False}}}
-        self.distro = "distrotest"  # generally should be a Distro object
+        self.distro = ubuntu.Distro("", {}, {})
         self.paths = Paths({})
         self.datasource = DataSource(self.sys_cfg, self.distro, self.paths)
 
@@ -201,7 +202,7 @@ class TestDataSource(CiTestCase):
         for log in expected_logs:
             self.assertIn(log, logs)
 
-    @mock.patch("cloudinit.sources.net.find_fallback_nic")
+    @mock.patch("cloudinit.distros.net.find_fallback_nic")
     def test_fallback_interface_is_discovered(self, m_get_fallback_nic):
         """The fallback_interface is discovered via find_fallback_nic."""
         m_get_fallback_nic.return_value = "nic9"
@@ -221,7 +222,7 @@ class TestDataSource(CiTestCase):
     @mock.patch("cloudinit.sources.net.find_fallback_nic")
     def test_wb_fallback_interface_is_cached(self, m_get_fallback_nic):
         """The fallback_interface is cached and won't be rediscovered."""
-        self.datasource._fallback_interface = "nic10"
+        self.datasource.distro._fallback_interface = "nic10"
         self.assertEqual("nic10", self.datasource.fallback_interface)
         m_get_fallback_nic.assert_not_called()
 
diff --git a/tests/unittests/sources/test_openstack.py b/tests/unittests/sources/test_openstack.py
index 127123cff..97cc8c94e 100644
--- a/tests/unittests/sources/test_openstack.py
+++ b/tests/unittests/sources/test_openstack.py
@@ -338,7 +338,7 @@ class TestOpenStackDataSource(test_helpers.ResponsesTestCase):
         ds_os_local = ds.DataSourceOpenStackLocal(
             settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp})
         )
-        ds_os_local._fallback_interface = "eth9"  # Monkey patch for dhcp
+        distro.fallback_interface = "eth9"  # Monkey patch for dhcp
         m_dhcp.return_value = {
             "interface": "eth9",
             "fixed-address": "192.168.2.9",