Repository: canonical/cloud-init (branch: holman/dhcpcd)
Commit date: 2024-01-06T01:46:19Z

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-06
  UV_INDEX_URL: http://localhost:8629/2024-01-06


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+77.g80f699fe.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.55.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.14,botocore==1.34.14,cachetools==5.3.2,certifi==2023.11.17,cffi==1.16.0,charset-normalizer==3.3.2,circuitbreaker==1.4.0,cliff==4.4.0,cloud-init==23.4+77.g80f699fe,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.112.0,google-auth==2.26.1,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.48.1,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.2,jmespath==1.0.1,jsonpatch==1.33,jsonpointer==2.4,jsonschema==4.20.0,jsonschema-specifications==2023.12.1,keystoneauth1==5.4.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.0,openstacksdk==1.4.0,os-service-types==1.7.0,osc-lib==2.9.0,oslo.config==9.2.0,oslo.i18n==6.2.0,oslo.serialization==5.2.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.2.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.16.2,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.12,wrapt==1.16.0,zipp==3.17.0
pylint run-test-pre: PYTHONHASHSEED='2276146897'
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:575: [W0612(unused-variable), Dhcpcd.dhcp_discovery] Unused variable 'out'
cloudinit/net/dhcp.py:575: [W0612(unused-variable), Dhcpcd.dhcp_discovery] Unused variable 'err'

------------------------------------
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.

step_name: Check black/6_Test.txt
log: ##[group]Run tox
[36;1mtox[0m
shell: /usr/bin/sh -ex {0}
env:
  TOXENV: black
##[endgroup]
+ tox
GLOB sdist-make: /home/runner/work/cloud-init/cloud-init/setup.py
black create: /home/runner/work/cloud-init/cloud-init/.tox/black
black installdeps: black===22.3.0
black inst: /home/runner/work/cloud-init/cloud-init/.tox/.tmp/package/1/cloud-init-23.4+77.g80f699fe.zip
black installed: attrs==23.2.0,black==22.3.0,certifi==2023.11.17,charset-normalizer==3.3.2,click==8.1.7,cloud-init==23.4+77.g80f699fe,configobj==5.0.8,idna==3.6,importlib-resources==6.1.1,Jinja2==3.1.2,jsonpatch==1.33,jsonpointer==2.4,jsonschema==4.20.0,jsonschema-specifications==2023.12.1,MarkupSafe==2.1.3,mypy-extensions==1.0.0,netifaces==0.11.0,oauthlib==3.2.2,pathspec==0.12.1,pkgutil-resolve-name==1.3.10,platformdirs==4.1.0,pyserial==3.5,PyYAML==6.0.1,referencing==0.32.1,requests==2.31.0,rpds-py==0.16.2,six==1.16.0,tomli==2.0.1,typing-extensions==4.9.0,urllib3==2.1.0,zipp==3.17.0
black run-test-pre: PYTHONHASHSEED='3554362748'
black run-test: commands[0] | /home/runner/work/cloud-init/cloud-init/.tox/black/bin/python -m black . --check
would reformat tests/unittests/net/test_dhcp.py

Oh no! 💥 💔 💥
1 file would be reformatted, 549 files would be left unchanged.
ERROR: InvocationError for command /home/runner/work/cloud-init/cloud-init/.tox/black/bin/python -m black . --check (exited with code 1)
___________________________________ summary ____________________________________
ERROR:   black: commands failed
##[error]Process completed with exit code 1.


Changed files:
cloudinit/net/dhcp.py
tests/unittests/net/test_dhcp.py

Fix diff:
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 1b8caee4f..6a4e5a3fa 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -175,6 +175,11 @@ class DhcpClient(abc.ABC):
     client_name = ""
     max_wait = 5
 
+    def __init__(self):
+        self.dhcp_client_path = subp.which(self.client_name)
+        if not self.dhcp_client_path:
+            raise NoDHCPLeaseMissingDhclientError()
+
     @classmethod
     def kill_dhcp_client(cls):
         subp.subp(["pkill", cls.client_name], rcs=[0, 1])
@@ -200,14 +205,6 @@ class DhcpClient(abc.ABC):
 class IscDhclient(DhcpClient):
     client_name = "dhclient"
 
-    def __init__(self):
-        self.dhclient_path = subp.which("dhclient")
-        if not self.dhclient_path:
-            LOG.debug(
-                "Skip dhclient configuration: No dhclient command found."
-            )
-            raise NoDHCPLeaseMissingDhclientError()
-
     @staticmethod
     def parse_dhcp_lease_file(lease_file: str) -> List[Dict[str, Any]]:
         """Parse the given dhcp lease file returning all leases as dicts.
@@ -298,7 +295,7 @@ class IscDhclient(DhcpClient):
         try:
             out, err = subp.subp(
                 distro.build_dhclient_cmd(
-                    self.dhclient_path,
+                    self.dhcp_client_path,
                     lease_file,
                     pid_file,
                     interface,
@@ -536,14 +533,6 @@ class IscDhclient(DhcpClient):
 class Dhcpcd:
     client_name = "dhcpcd"
 
-    def __init__(self):
-        self.dhclient_path = subp.which("dhcpcd")
-        if not self.dhclient_path:
-            LOG.debug(
-                "Skip dhclient configuration: No dhclient command found."
-            )
-            raise NoDHCPLeaseMissingDhclientError()
-
     def dhcp_discovery(
         self,
         interface,
@@ -572,9 +561,9 @@ class Dhcpcd:
         # /lib/dhcpcd/dhcpcd-hooks/ and pass each of those with the --nohook
         # argument to dhcpcd
         try:
-            out, err = subp.subp(
+            subp.subp(
                 [
-                    "dhcpcd",
+                    self.client_name,
                     "--oneshot",  # get lease then exit
                     "--nobackground",  # don't fork
                     "--ipv4only",  # only attempt configuring ipv4
@@ -584,6 +573,7 @@ class Dhcpcd:
                     interface,
                 ]
             )
+            return self.parse_dhcp_lease_file(interface)
         except subp.ProcessExecutionError as error:
             LOG.debug(
                 "dhclient exited with code: %s stderr: %r stdout: %r",
@@ -592,7 +582,6 @@ class Dhcpcd:
                 error.stdout,
             )
             raise NoDHCPLeaseError from error
-        return self.parse_dhcp_lease_file(interface)
 
     @staticmethod
     def parse_dhcpcd_lease(lease_dump: str, interface: str) -> List[dict]:
@@ -656,7 +645,7 @@ class Dhcpcd:
 
         except subp.ProcessExecutionError as error:
             LOG.debug(
-                "dhclient exited with code: %s stderr: %r stdout: %r",
+                "dhcpcd exited with code: %s stderr: %r stdout: %r",
                 error.exit_code,
                 error.stderr,
                 error.stdout,
@@ -667,12 +656,6 @@ class Dhcpcd:
 class Udhcpc(DhcpClient):
     client_name = "udhcpc"
 
-    def __init__(self):
-        self.udhcpc_path = subp.which("udhcpc")
-        if not self.udhcpc_path:
-            LOG.debug("Skip udhcpc configuration: No udhcpc command found.")
-            raise NoDHCPLeaseMissingUdhcpcError()
-
     def dhcp_discovery(
         self,
         interface,
@@ -702,7 +685,7 @@ class Udhcpc(DhcpClient):
         util.write_file(udhcpc_script, UDHCPC_SCRIPT, 0o755)
 
         cmd = [
-            self.udhcpc_path,
+            self.dhcp_client_path,
             "-O",
             "staticroutes",
             "-i",
diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py
index 2841a7244..fa8308068 100644
--- a/tests/unittests/net/test_dhcp.py
+++ b/tests/unittests/net/test_dhcp.py
@@ -8,6 +8,7 @@ import pytest
 import responses
 
 from cloudinit.net.dhcp import (
+    Dhcpcd,
     InvalidDHCPLeaseFileError,
     IscDhclient,
     NoDHCPLeaseError,
@@ -438,7 +439,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
             subp.ProcessExecutionError(exit_code=-5),
         ]
 
-        m_which.side_effect = [False, True]
+        m_which.side_effect = [False, False, False]
         with pytest.raises(NoDHCPLeaseError):
             maybe_perform_dhcp_discovery(MockDistro())
 
@@ -446,10 +447,6 @@ class TestDHCPDiscoveryClean(CiTestCase):
             "DHCP client not found: dhclient",
             self.logs.getvalue(),
         )
-        self.assertIn(
-           "DHCP client not found: dhcpcd",
-           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):
@@ -473,7 +470,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
             maybe_perform_dhcp_discovery(MockDistro())
 
         self.assertIn(
-            "Skip dhclient configuration: No dhclient command found.",
+            "DHCP client not found: dhclient",
             self.logs.getvalue(),
         )
 
@@ -975,24 +972,6 @@ class TestUDHCPCDiscoveryClean(CiTestCase):
     with_logs = True
     maxDiff = None
 
-    @mock.patch("cloudinit.net.dhcp.subp.which")
-    @mock.patch("cloudinit.net.dhcp.find_fallback_nic")
-    def test_absent_udhcpc_command(self, m_fallback, m_which):
-        """When dhclient doesn't exist in the OS, log the issue and no-op."""
-        m_fallback.return_value = "eth9"
-        m_which.return_value = None  # udhcpc isn't found
-
-        distro = MockDistro()
-        distro.dhcp_client_priority = [Udhcpc]
-
-        with pytest.raises(NoDHCPLeaseMissingDhclientError):
-            maybe_perform_dhcp_discovery(distro)
-
-        self.assertIn(
-            "Skip udhcpc configuration: No udhcpc command found.",
-            self.logs.getvalue(),
-        )
-
     @mock.patch("cloudinit.net.dhcp.is_ib_interface", return_value=False)
     @mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/udhcpc")
     @mock.patch("cloudinit.net.dhcp.os.remove")
@@ -1135,3 +1114,30 @@ class TestUDHCPCDiscoveryClean(CiTestCase):
                 ),
             ]
         )
+
+
+class TestDhcpcd:
+    def test_parse_lease(self):
+        lease = dedent(
+            """
+            broadcast_address='192.168.15.255'
+            dhcp_lease_time='3600'
+            dhcp_message_type='5'
+            dhcp_server_identifier='192.168.0.1'
+            domain_name='us-east-2.compute.internal'
+            domain_name_servers='192.168.0.2'
+            host_name='ip-192-168-0-212'
+            interface_mtu='9001'
+            ip_address='192.168.0.212'
+            network_number='192.168.0.0'
+            routers='192.168.0.1'
+            subnet_cidr='20'
+            subnet_mask='255.255.240.0'
+            """
+        )
+        parsed_lease = Dhcpcd.parse_dhcpcd_lease(lease, "eth0")[0]
+        assert "eth0" == parsed_lease["interface"]
+        assert "192.168.15.255" == parsed_lease["broadcast-address"]
+        assert "192.168.0.212" == parsed_lease["fixed-address"]
+        assert "255.255.240.0" == parsed_lease["subnet-mask"]
+        assert "192.168.0.1" == parsed_lease["routers"]