From f3ddeb00b85e2d526710e390dc6420c566d700a5 Mon Sep 17 00:00:00 2001 From: shixuantong Date: Tue, 27 Feb 2024 09:56:49 +0800 Subject: [PATCH] fix test failed (cherry picked from commit 88323bd0cafa413aac57443c958b375e31874548) --- ...eads-of-host-s-sys-class-net-via-get.patch | 73 ++ ...ix-unpin-jsonschema-and-update-tests.patch | 457 ++++++++++++ ...port-test-fix-disable_sysfs_net-mock.patch | 72 ++ ...-fix-tmpdir-in-test_cc_apk_configure.patch | 37 + ...rop-CiTestCase-and-convert-to-pytest.patch | 672 ++++++++++++++++++ cloud-init.spec | 48 +- 6 files changed, 1356 insertions(+), 3 deletions(-) create mode 100644 backport-bug-tests-mock-reads-of-host-s-sys-class-net-via-get.patch create mode 100644 backport-fix-unpin-jsonschema-and-update-tests.patch create mode 100644 backport-test-fix-disable_sysfs_net-mock.patch create mode 100644 backport-test-fix-tmpdir-in-test_cc_apk_configure.patch create mode 100644 backport-tests-drop-CiTestCase-and-convert-to-pytest.patch diff --git a/backport-bug-tests-mock-reads-of-host-s-sys-class-net-via-get.patch b/backport-bug-tests-mock-reads-of-host-s-sys-class-net-via-get.patch new file mode 100644 index 0000000..932fdb7 --- /dev/null +++ b/backport-bug-tests-mock-reads-of-host-s-sys-class-net-via-get.patch @@ -0,0 +1,73 @@ +From 463d36cc237435084e5ddb4d7468f5546e7ece28 Mon Sep 17 00:00:00 2001 +From: Chad Smith +Date: Wed, 6 Mar 2024 07:51:34 -0700 +Subject: [PATCH] bug(tests): mock reads of host's /sys/class/net via + get_sys_class_path + +Avoid leaking reads to the underlying host's /sys/class/net files. +Some test environments contain virtual network hardware and +configuration such as Calico network devices which provide +duplicated MAC addresses for each device in /sys/class/net. This +results in errors from unittests calling cloudinit.net.get_interfaces. + +Provide a disable_sysfs_net` fixture and use it in net-related +modules or functions to avoid unrelated test failures due to +environmental differences. + +Provide the ability to turn off this fixture for tests which +need to write to the mocked sysfs tmp directory so test artifacts +do not pollute other tests. + +This fixture can be disabled by passing False to the disable_sysfs_net +via request.param. We want to avoid polluting tox.ini with pytest.marks +for infrequently used cases like this. + +Also fix whitespace in tox.ini +--- + tests/unittests/conftest.py | 20 ++++++++++++++++++++ + tox.ini | 2 +- + 2 files changed, 21 insertions(+), 1 deletion(-) + +diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py +index 91dbd06..22bc189 100644 +--- a/tests/unittests/conftest.py ++++ b/tests/unittests/conftest.py +@@ -61,6 +61,26 @@ def fake_filesystem(mocker, tmpdir): + mocker.patch.object(mod, f, trap_func) + + ++@pytest.fixture(scope="session", autouse=True) ++def disable_sysfs_net(request, tmpdir_factory): ++ """Avoid tests which read the undertying host's /syc/class/net. ++ ++ To allow unobscured reads of /sys/class/net on the host we can ++ parametrize the fixture with: ++ ++ @pytest.mark.parametrize("disable_sysfs_net", [False], indirect=True) ++ """ ++ if hasattr(request, "param") and getattr(request, "param") is False: ++ # Test disabled this fixture, perform no mocks. ++ yield ++ return ++ mock_sysfs = f"{tmpdir_factory.mktemp('sysfs')}/" ++ with mock.patch( ++ "cloudinit.net.get_sys_class_path", return_value=mock_sysfs ++ ): ++ yield mock_sysfs ++ ++ + @pytest.fixture(autouse=True) + def disable_dns_lookup(request): + if "allow_dns_lookup" in request.keywords: +diff --git a/tox.ini b/tox.ini +index 5f01a9a..cd261f3 100644 +--- a/tox.ini ++++ b/tox.ini +@@ -323,4 +323,4 @@ markers = + serial: tests that do not work in parallel, skipped with py3-fast + unstable: skip this test because it is flakey + user_data: the user data to be passed to the test instance +- allow_dns_lookup: disable autochecking for host network configuration ++ allow_dns_lookup: disable autochecking for host network configuration +-- +2.33.0 diff --git a/backport-fix-unpin-jsonschema-and-update-tests.patch b/backport-fix-unpin-jsonschema-and-update-tests.patch new file mode 100644 index 0000000..1d1990f --- /dev/null +++ b/backport-fix-unpin-jsonschema-and-update-tests.patch @@ -0,0 +1,457 @@ +From c948e4182b9557f2befeb6210a055d0aa1c2df21 Mon Sep 17 00:00:00 2001 +From: James Falcon +Date: Wed, 14 Feb 2024 16:25:11 -0600 +Subject: [PATCH] fix: unpin jsonschema and update tests (#4882) + +In 034a5cd , we pinned jsonschema version due to failing tests. Test +failures were due to jsonschema library changing error messages. +This commit unpins the version and updates tests accordingly. + +Fixes GH-4783 + +Co-authored-by: dermotbradley +--- + .../unittests/config/test_cc_apk_configure.py | 5 +++-- + .../unittests/config/test_cc_apt_configure.py | 14 ++++++------- + tests/unittests/config/test_cc_bootcmd.py | 17 +++++++++++----- + tests/unittests/config/test_cc_ca_certs.py | 10 +++++++--- + tests/unittests/config/test_cc_chef.py | 5 +++-- + tests/unittests/config/test_cc_lxd.py | 2 +- + tests/unittests/config/test_cc_mounts.py | 10 ++++++++-- + .../test_cc_package_update_upgrade_install.py | 3 ++- + tests/unittests/config/test_cc_runcmd.py | 3 ++- + .../unittests/config/test_cc_set_passwords.py | 9 +++++++-- + tests/unittests/config/test_cc_snap.py | 20 +++++++++++++------ + tests/unittests/config/test_cc_write_files.py | 6 +++++- + .../unittests/config/test_cc_yum_add_repo.py | 2 +- + tests/unittests/helpers.py | 8 ++++++++ + tests/unittests/test_merging.py | 2 +- + 15 files changed, 81 insertions(+), 35 deletions(-) + +diff --git a/tests/unittests/config/test_cc_apk_configure.py b/tests/unittests/config/test_cc_apk_configure.py +index 8854670..e9533a1 100644 +--- a/tests/unittests/config/test_cc_apk_configure.py ++++ b/tests/unittests/config/test_cc_apk_configure.py +@@ -18,6 +18,7 @@ from cloudinit.config.schema import ( + validate_cloudconfig_schema, + ) + from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, + FilesystemMockingTestCase, + mock, + skipUnlessJsonSchema, +@@ -355,7 +356,7 @@ class TestApkConfigureSchema: + ( + {"apk_repos": {"alpine_repo": {}}}, + "apk_repos.alpine_repo: 'version' is a required property," +- " apk_repos.alpine_repo: {} does not have enough properties", ++ f" apk_repos.alpine_repo: {{}} {SCHEMA_EMPTY_ERROR}", + ), + ( + {"apk_repos": {"alpine_repo": True}}, +@@ -368,7 +369,7 @@ class TestApkConfigureSchema: + ), + ( + {"apk_repos": {}}, +- "apk_repos: {} does not have enough properties", ++ f"apk_repos: {{}} {SCHEMA_EMPTY_ERROR}", + ), + ( + {"apk_repos": {"local_repo_base_url": None}}, +diff --git a/tests/unittests/config/test_cc_apt_configure.py b/tests/unittests/config/test_cc_apt_configure.py +index 18e6663..4fff316 100644 +--- a/tests/unittests/config/test_cc_apt_configure.py ++++ b/tests/unittests/config/test_cc_apt_configure.py +@@ -12,7 +12,7 @@ from cloudinit.config.schema import ( + get_schema, + validate_cloudconfig_schema, + ) +-from tests.unittests.helpers import skipUnlessJsonSchema ++from tests.unittests.helpers import SCHEMA_EMPTY_ERROR, skipUnlessJsonSchema + from tests.unittests.util import get_cloud + + +@@ -34,7 +34,7 @@ class TestAPTConfigureSchema: + " ('boguskey' was unexpected)" + ), + ), +- ({"apt": {}}, "apt: {} does not have enough properties"), ++ ({"apt": {}}, f"apt: {{}} {SCHEMA_EMPTY_ERROR}"), + ( + {"apt": {"preserve_sources_list": 1}}, + "apt.preserve_sources_list: 1 is not of type 'boolean'", +@@ -45,7 +45,7 @@ class TestAPTConfigureSchema: + ), + ( + {"apt": {"disable_suites": []}}, +- re.escape("apt.disable_suites: [] is too short"), ++ re.escape("apt.disable_suites: [] ") + SCHEMA_EMPTY_ERROR, + ), + ( + {"apt": {"disable_suites": [1]}}, +@@ -65,7 +65,7 @@ class TestAPTConfigureSchema: + ), + ( + {"apt": {"primary": []}}, +- re.escape("apt.primary: [] is too short"), ++ re.escape("apt.primary: [] ") + SCHEMA_EMPTY_ERROR, + ), + ( + {"apt": {"primary": ["nonobj"]}}, +@@ -102,7 +102,7 @@ class TestAPTConfigureSchema: + ), + ( + {"apt": {"primary": [{"arches": ["amd64"], "search": []}]}}, +- re.escape("apt.primary.0.search: [] is too short"), ++ re.escape("apt.primary.0.search: [] ") + SCHEMA_EMPTY_ERROR, + ), + ( + { +@@ -134,7 +134,7 @@ class TestAPTConfigureSchema: + ), + ( + {"apt": {"debconf_selections": {}}}, +- "apt.debconf_selections: {} does not have enough properties", ++ f"apt.debconf_selections: {{}} {SCHEMA_EMPTY_ERROR}", + ), + ( + {"apt": {"sources_list": True}}, +@@ -170,7 +170,7 @@ class TestAPTConfigureSchema: + ), + ( + {"apt": {"sources": {"opaquekey": {}}}}, +- "apt.sources.opaquekey: {} does not have enough properties", ++ f"apt.sources.opaquekey: {{}} {SCHEMA_EMPTY_ERROR}", + ), + ( + {"apt": {"sources": {"opaquekey": {"boguskey": True}}}}, +diff --git a/tests/unittests/config/test_cc_bootcmd.py b/tests/unittests/config/test_cc_bootcmd.py +index f6772df..b938732 100644 +--- a/tests/unittests/config/test_cc_bootcmd.py ++++ b/tests/unittests/config/test_cc_bootcmd.py +@@ -11,7 +11,12 @@ from cloudinit.config.schema import ( + get_schema, + validate_cloudconfig_schema, + ) +-from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema ++from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, ++ CiTestCase, ++ mock, ++ skipUnlessJsonSchema, ++) + from tests.unittests.util import get_cloud + + +@@ -128,12 +133,14 @@ class TestBootCMDSchema: + "Cloud config schema errors: bootcmd: 1 is not of type" + " 'array'", + ), +- ({"bootcmd": []}, re.escape("bootcmd: [] is too short")), + ( + {"bootcmd": []}, +- re.escape( +- "Cloud config schema errors: bootcmd: [] is too short" +- ), ++ re.escape("bootcmd: [] ") + SCHEMA_EMPTY_ERROR, ++ ), ++ ( ++ {"bootcmd": []}, ++ re.escape("Cloud config schema errors: bootcmd: [] ") ++ + SCHEMA_EMPTY_ERROR, + ), + ( + { +diff --git a/tests/unittests/config/test_cc_ca_certs.py b/tests/unittests/config/test_cc_ca_certs.py +index b93fda7..cb5712b 100644 +--- a/tests/unittests/config/test_cc_ca_certs.py ++++ b/tests/unittests/config/test_cc_ca_certs.py +@@ -17,7 +17,11 @@ from cloudinit.config.schema import ( + get_schema, + validate_cloudconfig_schema, + ) +-from tests.unittests.helpers import TestCase, skipUnlessJsonSchema ++from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, ++ TestCase, ++ skipUnlessJsonSchema, ++) + from tests.unittests.util import get_cloud + + +@@ -398,7 +402,7 @@ class TestCACertsSchema: + ), + ( + {"ca_certs": {}}, +- re.escape("ca_certs: {} does not have enough properties"), ++ re.escape("ca_certs: {} ") + SCHEMA_EMPTY_ERROR, + ), + ( + {"ca_certs": {"boguskey": 1}}, +@@ -417,7 +421,7 @@ class TestCACertsSchema: + ), + ( + {"ca_certs": {"trusted": []}}, +- re.escape("ca_certs.trusted: [] is too short"), ++ re.escape("ca_certs.trusted: [] ") + SCHEMA_EMPTY_ERROR, + ), + ), + ) +diff --git a/tests/unittests/config/test_cc_chef.py b/tests/unittests/config/test_cc_chef.py +index 9d8ba1f..6fad6a7 100644 +--- a/tests/unittests/config/test_cc_chef.py ++++ b/tests/unittests/config/test_cc_chef.py +@@ -15,6 +15,7 @@ from cloudinit.config.schema import ( + validate_cloudconfig_schema, + ) + from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, + FilesystemMockingTestCase, + ResponsesTestCase, + cloud_init_project_dir, +@@ -306,7 +307,7 @@ class TestBootCMDSchema: + ), + ( + {"chef": {}}, +- re.escape(" chef: {} does not have enough properties"), ++ re.escape(" chef: {} ") + SCHEMA_EMPTY_ERROR, + ), + ( + {"chef": {"boguskey": True}}, +@@ -321,7 +322,7 @@ class TestBootCMDSchema: + ), + ( + {"chef": {"directories": []}}, +- re.escape("chef.directories: [] is too short"), ++ re.escape("chef.directories: [] ") + SCHEMA_EMPTY_ERROR, + ), + ( + {"chef": {"directories": [1]}}, +diff --git a/tests/unittests/config/test_cc_lxd.py b/tests/unittests/config/test_cc_lxd.py +index 648fc33..320bfdb 100644 +--- a/tests/unittests/config/test_cc_lxd.py ++++ b/tests/unittests/config/test_cc_lxd.py +@@ -385,7 +385,7 @@ class TestLXDSchema: + # Require bridge.mode + ({"lxd": {"bridge": {}}}, "bridge: 'mode' is a required property"), + # Require init or bridge keys +- ({"lxd": {}}, "lxd: {} does not have enough properties"), ++ ({"lxd": {}}, f"lxd: {{}} {t_help.SCHEMA_EMPTY_ERROR}"), + # Require some non-empty preseed config of type string + ({"lxd": {"preseed": {}}}, "not of type 'string'"), + ({"lxd": {"preseed": ""}}, None), +diff --git a/tests/unittests/config/test_cc_mounts.py b/tests/unittests/config/test_cc_mounts.py +index a2dada8..4795357 100644 +--- a/tests/unittests/config/test_cc_mounts.py ++++ b/tests/unittests/config/test_cc_mounts.py +@@ -583,9 +583,15 @@ class TestMountsSchema: + "config, error_msg", + [ + # We expect to see one mount if provided in user-data. +- ({"mounts": []}, re.escape("mounts: [] is too short")), ++ ( ++ {"mounts": []}, ++ re.escape("mounts: [] ") + test_helpers.SCHEMA_EMPTY_ERROR, ++ ), + # Disallow less than 1 item per mount entry +- ({"mounts": [[]]}, re.escape("mounts.0: [] is too short")), ++ ( ++ {"mounts": [[]]}, ++ re.escape("mounts.0: [] ") + test_helpers.SCHEMA_EMPTY_ERROR, ++ ), + # Disallow more than 6 items per mount entry + ({"mounts": [["1"] * 7]}, "mounts.0:.* is too long"), + # Disallow mount_default_fields will anything other than 6 items +diff --git a/tests/unittests/config/test_cc_package_update_upgrade_install.py b/tests/unittests/config/test_cc_package_update_upgrade_install.py +index 9ba7f17..b3d43a1 100644 +--- a/tests/unittests/config/test_cc_package_update_upgrade_install.py ++++ b/tests/unittests/config/test_cc_package_update_upgrade_install.py +@@ -14,6 +14,7 @@ from cloudinit.config.schema import ( + from cloudinit.distros import PackageInstallerError + from cloudinit.subp import SubpResult + from tests.unittests.helpers import skipUnlessJsonSchema ++from tests.unittests.helpers import SCHEMA_EMPTY_ERROR + from tests.unittests.util import get_cloud + + +@@ -188,7 +189,7 @@ class TestPackageUpdateUpgradeSchema: + # packages list with three entries (2 required) + ({"packages": ["p1", ["p2", "p3", "p4"]]}, ""), + # empty packages list +- ({"packages": []}, "is too short"), ++ ({"packages": []}, SCHEMA_EMPTY_ERROR), + ( + {"apt_update": False}, + ( +diff --git a/tests/unittests/config/test_cc_runcmd.py b/tests/unittests/config/test_cc_runcmd.py +index a8636bb..f69cd9d 100644 +--- a/tests/unittests/config/test_cc_runcmd.py ++++ b/tests/unittests/config/test_cc_runcmd.py +@@ -14,6 +14,7 @@ from cloudinit.config.schema import ( + validate_cloudconfig_schema, + ) + from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, + FilesystemMockingTestCase, + skipUnlessJsonSchema, + ) +@@ -90,7 +91,7 @@ class TestRunCmdSchema: + ({"runcmd": ["echo bye", "echo bye"]}, None), + # Invalid schemas + ({"runcmd": 1}, "1 is not of type 'array'"), +- ({"runcmd": []}, r"runcmd: \[\] is too short"), ++ ({"runcmd": []}, rf"runcmd: \[\] {SCHEMA_EMPTY_ERROR}"), + ( + { + "runcmd": [ +diff --git a/tests/unittests/config/test_cc_set_passwords.py b/tests/unittests/config/test_cc_set_passwords.py +index 1a9fcd3..ef34a8c 100644 +--- a/tests/unittests/config/test_cc_set_passwords.py ++++ b/tests/unittests/config/test_cc_set_passwords.py +@@ -12,7 +12,11 @@ from cloudinit.config.schema import ( + get_schema, + validate_cloudconfig_schema, + ) +-from tests.unittests.helpers import does_not_raise, skipUnlessJsonSchema ++from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, ++ does_not_raise, ++ skipUnlessJsonSchema, ++) + from tests.unittests.util import get_cloud + + MODPATH = "cloudinit.config.cc_set_passwords." +@@ -718,7 +722,8 @@ class TestSetPasswordsSchema: + ( + {"chpasswd": {"list": []}}, + pytest.raises( +- SchemaValidationError, match=r"\[\] is too short" ++ SchemaValidationError, ++ match=rf"\[\] {SCHEMA_EMPTY_ERROR}", + ), + ), + ], +diff --git a/tests/unittests/config/test_cc_snap.py b/tests/unittests/config/test_cc_snap.py +index 65088dd..573ade9 100644 +--- a/tests/unittests/config/test_cc_snap.py ++++ b/tests/unittests/config/test_cc_snap.py +@@ -14,7 +14,12 @@ from cloudinit.config.schema import ( + get_schema, + validate_cloudconfig_schema, + ) +-from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema ++from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, ++ CiTestCase, ++ mock, ++ skipUnlessJsonSchema, ++) + from tests.unittests.util import get_cloud + + M_PATH = "cloudinit.config.cc_snap." +@@ -288,15 +293,18 @@ class TestSnapSchema: + {"snap": {"commands": ["ls"], "invalid-key": ""}}, + "Additional properties are not allowed", + ), +- ({"snap": {}}, "{} does not have enough properties"), ++ ({"snap": {}}, f"{{}} {SCHEMA_EMPTY_ERROR}"), + ( + {"snap": {"commands": "broken"}}, + "'broken' is not of type 'object', 'array'", + ), +- ({"snap": {"commands": []}}, r"snap.commands: \[\] is too short"), ++ ( ++ {"snap": {"commands": []}}, ++ rf"snap.commands: \[\] {SCHEMA_EMPTY_ERROR}", ++ ), + ( + {"snap": {"commands": {}}}, +- r"snap.commands: {} does not have enough properties", ++ rf"snap.commands: {{}} {SCHEMA_EMPTY_ERROR}", + ), + ({"snap": {"commands": [123]}}, ""), + ({"snap": {"commands": {"01": 123}}}, ""), +@@ -311,10 +319,10 @@ class TestSnapSchema: + {"snap": {"assertions": "broken"}}, + "'broken' is not of type 'object', 'array'", + ), +- ({"snap": {"assertions": []}}, r"\[\] is too short"), ++ ({"snap": {"assertions": []}}, rf"\[\] {SCHEMA_EMPTY_ERROR}"), + ( + {"snap": {"assertions": {}}}, +- r"\{} does not have enough properties", ++ rf"\{{}} {SCHEMA_EMPTY_ERROR}", + ), + ], + ) +diff --git a/tests/unittests/config/test_cc_write_files.py b/tests/unittests/config/test_cc_write_files.py +index 210edf7..ea9cf8a 100644 +--- a/tests/unittests/config/test_cc_write_files.py ++++ b/tests/unittests/config/test_cc_write_files.py +@@ -18,6 +18,7 @@ from cloudinit.config.schema import ( + validate_cloudconfig_schema, + ) + from tests.unittests.helpers import ( ++ SCHEMA_EMPTY_ERROR, + CiTestCase, + FilesystemMockingTestCase, + skipUnlessJsonSchema, +@@ -222,7 +223,10 @@ class TestWriteFilesSchema: + [ + # Top-level write_files type validation + ({"write_files": 1}, "write_files: 1 is not of type 'array'"), +- ({"write_files": []}, re.escape("write_files: [] is too short")), ++ ( ++ {"write_files": []}, ++ re.escape("write_files: [] ") + SCHEMA_EMPTY_ERROR, ++ ), + ( + {"write_files": [{}]}, + "write_files.0: 'path' is a required property", +diff --git a/tests/unittests/config/test_cc_yum_add_repo.py b/tests/unittests/config/test_cc_yum_add_repo.py +index d2c2912..e7392f2 100644 +--- a/tests/unittests/config/test_cc_yum_add_repo.py ++++ b/tests/unittests/config/test_cc_yum_add_repo.py +@@ -139,7 +139,7 @@ class TestAddYumRepoSchema: + ), + ( + {"yum_repos": {}}, +- re.escape("yum_repos: {} does not have enough properties"), ++ re.escape("yum_repos: {} ") + helpers.SCHEMA_EMPTY_ERROR, + ), + # baseurl required + ( +diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py +index 96f407f..42cc2e9 100644 +--- a/tests/unittests/helpers.py ++++ b/tests/unittests/helpers.py +@@ -47,6 +47,14 @@ except ImportError: + HAS_APT_PKG = False + + ++# Used by tests to verify the error message when a jsonschema structure ++# is empty but should not be. ++# Version 4.20.0 of jsonschema changed the error messages for empty structures. ++SCHEMA_EMPTY_ERROR = ( ++ "(is too short|should be non-empty|does not have enough properties)" ++) ++ ++ + # Makes the old path start + # with new base instead of whatever + # it previously had +diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py +index 891031e..52abaa7 100644 +--- a/tests/unittests/test_merging.py ++++ b/tests/unittests/test_merging.py +@@ -270,7 +270,7 @@ class TestMergingSchema: + [ + ({"merge_how": "list()+dict()+str()"}, None), + ({"merge_type": "list()+dict()+str()"}, None), +- ({"merge_how": []}, "\\[\\] is too short"), ++ ({"merge_how": []}, f"\\[\\] {helpers.SCHEMA_EMPTY_ERROR}"), + ( + {"merge_how": {"name": "list", "settings": ["append"]}}, + "is not of type", +-- +2.27.0 diff --git a/backport-test-fix-disable_sysfs_net-mock.patch b/backport-test-fix-disable_sysfs_net-mock.patch new file mode 100644 index 0000000..1f334a6 --- /dev/null +++ b/backport-test-fix-disable_sysfs_net-mock.patch @@ -0,0 +1,72 @@ +From dc0eafbc7d88be99e11301081adb41ad7b50338e Mon Sep 17 00:00:00 2001 +From: James Falcon +Date: Mon, 11 Mar 2024 19:37:48 -0500 +Subject: [PATCH] test: fix `disable_sysfs_net` mock (#5065) + +The fixture parametrization ability added in 9baf31c doesn't work +as expected. When you have a session-wide fixture, the setup is run +once, then further invocations of the fixture (including autouse) uses a +cached version of the fixture. Teardown for the session fixture happens +at the end of all test runs. This also applies to mock patching. Since +the mock patching happens only once, parametrizing the fixture to yield +without patching doesn't undo the initial mock setup; the +parametrization of `disable_sys_net` effectively does nothing. + +The good news is that patches stack, so current tests that patch +`get_sys_class_path` differently will still work fine. If we need to +disable the patching entirely, that is also possible by saving the +original `get_sys_class_path` before applying the global disable mock, +then having a separate mock that has a side effect of calling +the original function. + +Reference:https://github.com/canonical/cloud-init/commit/dc0eafbc7d88be99e11301081adb41ad7b50338e +Conflict:NA +--- + tests/unittests/conftest.py | 14 ++------------ + tests/unittests/net/test_init.py | 5 +---- + 2 files changed, 3 insertions(+), 16 deletions(-) + +diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py +index 22bc189..bdd21c3 100644 +--- a/tests/unittests/conftest.py ++++ b/tests/unittests/conftest.py +@@ -62,18 +62,8 @@ def fake_filesystem(mocker, tmpdir): + + + @pytest.fixture(scope="session", autouse=True) +-def disable_sysfs_net(request, tmpdir_factory): +- """Avoid tests which read the undertying host's /syc/class/net. +- +- To allow unobscured reads of /sys/class/net on the host we can +- parametrize the fixture with: +- +- @pytest.mark.parametrize("disable_sysfs_net", [False], indirect=True) +- """ +- if hasattr(request, "param") and getattr(request, "param") is False: +- # Test disabled this fixture, perform no mocks. +- yield +- return ++def disable_sysfs_net(tmpdir_factory): ++ """Avoid tests which read the underlying host's /syc/class/net.""" + mock_sysfs = f"{tmpdir_factory.mktemp('sysfs')}/" + with mock.patch( + "cloudinit.net.get_sys_class_path", return_value=mock_sysfs +diff --git a/tests/unittests/net/test_init.py b/tests/unittests/net/test_init.py +index a7b75ab..51e54d0 100644 +--- a/tests/unittests/net/test_init.py ++++ b/tests/unittests/net/test_init.py +@@ -42,10 +42,7 @@ class TestSysDevPath: + + class TestReadSysNet: + @pytest.fixture(autouse=True) +- @pytest.mark.parametrize( +- "disable_sysfs_net", [False], indirect=["disable_sysfs_net"] +- ) +- def setup(self, disable_sysfs_net, tmpdir_factory): ++ def setup(self, tmpdir_factory): + # We mock invididual numbered tmpdirs here because these tests write + # to the sysfs directory and stale test artifacts break later tests. + mock_sysfs = f"{tmpdir_factory.mktemp('sysfs', numbered=True)}/" +-- +2.43.0 + diff --git a/backport-test-fix-tmpdir-in-test_cc_apk_configure.patch b/backport-test-fix-tmpdir-in-test_cc_apk_configure.patch new file mode 100644 index 0000000..f7672ae --- /dev/null +++ b/backport-test-fix-tmpdir-in-test_cc_apk_configure.patch @@ -0,0 +1,37 @@ +From 4ed78b116774dcbf78cfc556c823bdb7f8384068 Mon Sep 17 00:00:00 2001 +From: James Falcon +Date: Tue, 20 Feb 2024 11:08:13 -0600 +Subject: [PATCH] test: fix tmpdir in test_cc_apk_configure (#4914) + +cc_apk_configure uses temp_utils.py, which has special logic to return +if the user is root that can't be retargeted using our current fixtures. +Fix it by setting that tmpdir in the test setup. +--- + tests/unittests/config/test_cc_apk_configure.py | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/tests/unittests/config/test_cc_apk_configure.py b/tests/unittests/config/test_cc_apk_configure.py +index 72ed82242ee..47777b4701a 100644 +--- a/tests/unittests/config/test_cc_apk_configure.py ++++ b/tests/unittests/config/test_cc_apk_configure.py +@@ -10,7 +10,7 @@ + + import pytest + +-from cloudinit import cloud, helpers, util ++from cloudinit import cloud, helpers, temp_utils, util + from cloudinit.config import cc_apk_configure + from cloudinit.config.schema import ( + SchemaValidationError, +@@ -60,6 +60,11 @@ def setUp(self): + self.name = "apk_configure" + self.cloud = cloud.Cloud(None, self.paths, None, None, None) + self.args = [] ++ temp_utils._TMPDIR = self.new_root ++ ++ def tearDown(self): ++ super().tearDown() ++ temp_utils._TMPDIR = None + + @mock.patch(CC_APK + "._write_repositories_file") + def test_no_repo_settings(self, m_write_repos): diff --git a/backport-tests-drop-CiTestCase-and-convert-to-pytest.patch b/backport-tests-drop-CiTestCase-and-convert-to-pytest.patch new file mode 100644 index 0000000..62b3505 --- /dev/null +++ b/backport-tests-drop-CiTestCase-and-convert-to-pytest.patch @@ -0,0 +1,672 @@ +From 9baf31c7108b031d469e6476b949e042aba249ea Mon Sep 17 00:00:00 2001 +From: Chad Smith +Date: Wed, 6 Mar 2024 09:47:36 -0700 +Subject: [PATCH] tests: drop CiTestCase and convert to pytest + +Prevent disable_sysfs_net fixture from impacting TestSysDevPath +and TestReadSysNet in order to avoid shared mocks of /sys/class/net. +This avoid test artifact pollution for TestReadSysNet. + +Adapt the following tests, dropping CiTestCase to use pytest: + TestDHCPDiscoveryClean, TestSysDevPath, TestReadSysNet, + TestGenerateFallbackConfig, TestNetFailOver, TestConvertNetJson + +Fixes GH-4973 + +Reference:https://github.com/canonical/cloud-init/commit/9baf31c7108b031d469e6476b949e042aba249ea +Confilct:don't change test_dhcp.py, otherwise, it will introduce many test case failures (eg. +test_provided_nic_does_not_exist). Some failed tests will be deleted in +21b2b6e4423b0fec325b32042c05ef4274cdd301. +--- + tests/unittests/net/test_init.py | 298 +++++++++--------- + .../sources/helpers/test_openstack.py | 17 +- + 2 files changed, 149 insertions(+), 166 deletions(-) + +diff --git a/tests/unittests/net/test_init.py b/tests/unittests/net/test_init.py +index 561d515..a7b75ab 100644 +--- a/tests/unittests/net/test_init.py ++++ b/tests/unittests/net/test_init.py +@@ -17,49 +17,55 @@ from cloudinit import subp + from cloudinit.net.ephemeral import EphemeralIPv4Network, EphemeralIPv6Network + from cloudinit.subp import ProcessExecutionError + from cloudinit.util import ensure_file, write_file +-from tests.unittests.helpers import CiTestCase, ResponsesTestCase ++from tests.unittests.helpers import ( ++ CiTestCase, ++ ResponsesTestCase, ++ random_string, ++) + from tests.unittests.util import MockDistro + + +-class TestSysDevPath(CiTestCase): ++class TestSysDevPath: + def test_sys_dev_path(self): + """sys_dev_path returns a path under SYS_CLASS_NET for a device.""" + dev = "something" + path = "attribute" +- expected = net.SYS_CLASS_NET + dev + "/" + path +- self.assertEqual(expected, net.sys_dev_path(dev, path)) ++ expected = net.get_sys_class_path() + dev + "/" + path ++ assert expected == net.sys_dev_path(dev, path) + + def test_sys_dev_path_without_path(self): + """When path param isn't provided it defaults to empty string.""" + dev = "something" +- expected = net.SYS_CLASS_NET + dev + "/" +- self.assertEqual(expected, net.sys_dev_path(dev)) +- ++ expected = net.get_sys_class_path() + dev + "/" ++ assert expected == net.sys_dev_path(dev) + +-class TestReadSysNet(CiTestCase): +- with_logs = True + +- def setUp(self): +- super(TestReadSysNet, self).setUp() +- sys_mock = mock.patch("cloudinit.net.get_sys_class_path") +- self.m_sys_path = sys_mock.start() +- self.sysdir = self.tmp_dir() + "/" +- self.m_sys_path.return_value = self.sysdir +- self.addCleanup(sys_mock.stop) ++class TestReadSysNet: ++ @pytest.fixture(autouse=True) ++ @pytest.mark.parametrize( ++ "disable_sysfs_net", [False], indirect=["disable_sysfs_net"] ++ ) ++ def setup(self, disable_sysfs_net, tmpdir_factory): ++ # We mock invididual numbered tmpdirs here because these tests write ++ # to the sysfs directory and stale test artifacts break later tests. ++ mock_sysfs = f"{tmpdir_factory.mktemp('sysfs', numbered=True)}/" ++ with mock.patch( ++ "cloudinit.net.get_sys_class_path", return_value=mock_sysfs ++ ): ++ self.sysdir = mock_sysfs ++ yield + + def test_read_sys_net_strips_contents_of_sys_path(self): + """read_sys_net strips whitespace from the contents of a sys file.""" + content = "some stuff with trailing whitespace\t\r\n" + write_file(os.path.join(self.sysdir, "dev", "attr"), content) +- self.assertEqual(content.strip(), net.read_sys_net("dev", "attr")) ++ assert content.strip() == net.read_sys_net("dev", "attr") + + def test_read_sys_net_reraises_oserror(self): + """read_sys_net raises OSError/IOError when file doesn't exist.""" + # Non-specific Exception because versions of python OSError vs IOError. +- with self.assertRaises(Exception) as context_manager: # noqa: H202 ++ with pytest.raises(Exception, match="No such file or directory"): + net.read_sys_net("dev", "attr") +- error = context_manager.exception +- self.assertIn("No such file or directory", str(error)) + + def test_read_sys_net_handles_error_with_on_enoent(self): + """read_sys_net handles OSError/IOError with on_enoent if provided.""" +@@ -70,30 +76,27 @@ class TestReadSysNet(CiTestCase): + + net.read_sys_net("dev", "attr", on_enoent=on_enoent) + error = handled_errors[0] +- self.assertIsInstance(error, Exception) +- self.assertIn("No such file or directory", str(error)) ++ assert isinstance(error, Exception) ++ assert "No such file or directory" in str(error) + + def test_read_sys_net_translates_content(self): + """read_sys_net translates content when translate dict is provided.""" + content = "you're welcome\n" + write_file(os.path.join(self.sysdir, "dev", "attr"), content) + translate = {"you're welcome": "de nada"} +- self.assertEqual( +- "de nada", net.read_sys_net("dev", "attr", translate=translate) ++ assert "de nada" == net.read_sys_net( ++ "dev", "attr", translate=translate + ) + +- def test_read_sys_net_errors_on_translation_failures(self): ++ def test_read_sys_net_errors_on_translation_failures(self, caplog): + """read_sys_net raises a KeyError and logs details on failure.""" + content = "you're welcome\n" + write_file(os.path.join(self.sysdir, "dev", "attr"), content) +- with self.assertRaises(KeyError) as context_manager: ++ with pytest.raises(KeyError, match='"you\'re welcome"'): + net.read_sys_net("dev", "attr", translate={}) +- error = context_manager.exception +- self.assertEqual('"you\'re welcome"', str(error)) +- self.assertIn( ++ assert ( + "Found unexpected (not translatable) value 'you're welcome' in " +- "'{0}dev/attr".format(self.sysdir), +- self.logs.getvalue(), ++ "'{0}dev/attr".format(self.sysdir) in caplog.text + ) + + def test_read_sys_net_handles_handles_with_onkeyerror(self): +@@ -107,63 +110,63 @@ class TestReadSysNet(CiTestCase): + + net.read_sys_net("dev", "attr", translate={}, on_keyerror=on_keyerror) + error = handled_errors[0] +- self.assertIsInstance(error, KeyError) +- self.assertEqual('"you\'re welcome"', str(error)) ++ assert isinstance(error, KeyError) ++ assert '"you\'re welcome"' == str(error) + + def test_read_sys_net_safe_false_on_translate_failure(self): + """read_sys_net_safe returns False on translation failures.""" + content = "you're welcome\n" + write_file(os.path.join(self.sysdir, "dev", "attr"), content) +- self.assertFalse(net.read_sys_net_safe("dev", "attr", translate={})) ++ assert not net.read_sys_net_safe("dev", "attr", translate={}) + + def test_read_sys_net_safe_returns_false_on_noent_failure(self): + """read_sys_net_safe returns False on file not found failures.""" +- self.assertFalse(net.read_sys_net_safe("dev", "attr")) ++ assert not net.read_sys_net_safe("dev", "attr") + + def test_read_sys_net_int_returns_none_on_error(self): + """read_sys_net_safe returns None on failures.""" +- self.assertFalse(net.read_sys_net_int("dev", "attr")) ++ assert not net.read_sys_net_int("dev", "attr") + + def test_read_sys_net_int_returns_none_on_valueerror(self): + """read_sys_net_safe returns None when content is not an int.""" + write_file(os.path.join(self.sysdir, "dev", "attr"), "NOTINT\n") +- self.assertFalse(net.read_sys_net_int("dev", "attr")) ++ assert not net.read_sys_net_int("dev", "attr") + + def test_read_sys_net_int_returns_integer_from_content(self): + """read_sys_net_safe returns None on failures.""" + write_file(os.path.join(self.sysdir, "dev", "attr"), "1\n") +- self.assertEqual(1, net.read_sys_net_int("dev", "attr")) ++ assert 1 == net.read_sys_net_int("dev", "attr") + + def test_is_up_true(self): + """is_up is True if sys/net/devname/operstate is 'up' or 'unknown'.""" + for state in ["up", "unknown"]: + write_file(os.path.join(self.sysdir, "eth0", "operstate"), state) +- self.assertTrue(net.is_up("eth0")) ++ assert net.is_up("eth0") + + def test_is_up_false(self): + """is_up is False if sys/net/devname/operstate is 'down' or invalid.""" + for state in ["down", "incomprehensible"]: + write_file(os.path.join(self.sysdir, "eth0", "operstate"), state) +- self.assertFalse(net.is_up("eth0")) ++ assert not net.is_up("eth0") + + def test_is_bridge(self): + """is_bridge is True when /sys/net/devname/bridge exists.""" +- self.assertFalse(net.is_bridge("eth0")) ++ assert not net.is_bridge("eth0") + ensure_file(os.path.join(self.sysdir, "eth0", "bridge")) +- self.assertTrue(net.is_bridge("eth0")) ++ assert net.is_bridge("eth0") + + def test_is_bond(self): + """is_bond is True when /sys/net/devname/bonding exists.""" +- self.assertFalse(net.is_bond("eth0")) ++ assert not net.is_bond("eth0") + ensure_file(os.path.join(self.sysdir, "eth0", "bonding")) +- self.assertTrue(net.is_bond("eth0")) ++ assert net.is_bond("eth0") + + def test_get_master(self): + """get_master returns the path when /sys/net/devname/master exists.""" +- self.assertIsNone(net.get_master("enP1s1")) ++ assert net.get_master("enP1s1") is None + master_path = os.path.join(self.sysdir, "enP1s1", "master") + ensure_file(master_path) +- self.assertEqual(master_path, net.get_master("enP1s1")) ++ assert master_path == net.get_master("enP1s1") + + def test_master_is_bridge_or_bond(self): + bridge_mac = "aa:bb:cc:aa:bb:cc" +@@ -173,8 +176,8 @@ class TestReadSysNet(CiTestCase): + write_file(os.path.join(self.sysdir, "eth1", "address"), bridge_mac) + write_file(os.path.join(self.sysdir, "eth2", "address"), bond_mac) + +- self.assertFalse(net.master_is_bridge_or_bond("eth1")) +- self.assertFalse(net.master_is_bridge_or_bond("eth2")) ++ assert not net.master_is_bridge_or_bond("eth1") ++ assert not net.master_is_bridge_or_bond("eth2") + + # masters without bridge/bonding => False + write_file(os.path.join(self.sysdir, "br0", "address"), bridge_mac) +@@ -183,15 +186,15 @@ class TestReadSysNet(CiTestCase): + os.symlink("../br0", os.path.join(self.sysdir, "eth1", "master")) + os.symlink("../bond0", os.path.join(self.sysdir, "eth2", "master")) + +- self.assertFalse(net.master_is_bridge_or_bond("eth1")) +- self.assertFalse(net.master_is_bridge_or_bond("eth2")) ++ assert not net.master_is_bridge_or_bond("eth1") ++ assert not net.master_is_bridge_or_bond("eth2") + + # masters with bridge/bonding => True + write_file(os.path.join(self.sysdir, "br0", "bridge"), "") + write_file(os.path.join(self.sysdir, "bond0", "bonding"), "") + +- self.assertTrue(net.master_is_bridge_or_bond("eth1")) +- self.assertTrue(net.master_is_bridge_or_bond("eth2")) ++ assert net.master_is_bridge_or_bond("eth1") ++ assert net.master_is_bridge_or_bond("eth2") + + def test_master_is_openvswitch(self): + ovs_mac = "bb:cc:aa:bb:cc:aa" +@@ -199,7 +202,7 @@ class TestReadSysNet(CiTestCase): + # No master => False + write_file(os.path.join(self.sysdir, "eth1", "address"), ovs_mac) + +- self.assertFalse(net.master_is_bridge_or_bond("eth1")) ++ assert not net.master_is_bridge_or_bond("eth1") + + # masters without ovs-system => False + write_file(os.path.join(self.sysdir, "ovs-system", "address"), ovs_mac) +@@ -208,7 +211,7 @@ class TestReadSysNet(CiTestCase): + "../ovs-system", os.path.join(self.sysdir, "eth1", "master") + ) + +- self.assertFalse(net.master_is_openvswitch("eth1")) ++ assert not net.master_is_openvswitch("eth1") + + # masters with ovs-system => True + os.symlink( +@@ -216,15 +219,15 @@ class TestReadSysNet(CiTestCase): + os.path.join(self.sysdir, "eth1", "upper_ovs-system"), + ) + +- self.assertTrue(net.master_is_openvswitch("eth1")) ++ assert net.master_is_openvswitch("eth1") + + def test_is_vlan(self): + """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan.""" + ensure_file(os.path.join(self.sysdir, "eth0", "uevent")) +- self.assertFalse(net.is_vlan("eth0")) ++ assert not net.is_vlan("eth0") + content = "junk\nDEVTYPE=vlan\njunk\n" + write_file(os.path.join(self.sysdir, "eth0", "uevent"), content) +- self.assertTrue(net.is_vlan("eth0")) ++ assert net.is_vlan("eth0") + + + class TestGenerateFallbackConfig(CiTestCase): +@@ -1453,134 +1456,121 @@ class TestExtractPhysdevs(CiTestCase): + net.extract_physdevs({"version": 3, "awesome_config": []}) + + +-class TestNetFailOver(CiTestCase): +- def setUp(self): +- super(TestNetFailOver, self).setUp() +- self.add_patch("cloudinit.net.util", "m_util") +- self.add_patch("cloudinit.net.read_sys_net", "m_read_sys_net") +- self.add_patch("cloudinit.net.device_driver", "m_device_driver") ++class TestNetFailOver: ++ @pytest.fixture(autouse=True) ++ def setup(self, mocker): ++ mocker.patch("cloudinit.net.util") ++ self.device_driver = mocker.patch("cloudinit.net.device_driver") ++ self.read_sys_net = mocker.patch("cloudinit.net.read_sys_net") + + def test_get_dev_features(self): +- devname = self.random_string() +- features = self.random_string() +- self.m_read_sys_net.return_value = features ++ devname = random_string() ++ features = random_string() ++ self.read_sys_net.return_value = features + +- self.assertEqual(features, net.get_dev_features(devname)) +- self.assertEqual(1, self.m_read_sys_net.call_count) +- self.assertEqual( +- mock.call(devname, "device/features"), +- self.m_read_sys_net.call_args_list[0], +- ) ++ assert features == net.get_dev_features(devname) ++ assert 1 == self.read_sys_net.call_count ++ self.read_sys_net.assert_called_once_with(devname, "device/features") + + def test_get_dev_features_none_returns_empty_string(self): +- devname = self.random_string() +- self.m_read_sys_net.side_effect = Exception("error") +- self.assertEqual("", net.get_dev_features(devname)) +- self.assertEqual(1, self.m_read_sys_net.call_count) +- self.assertEqual( +- mock.call(devname, "device/features"), +- self.m_read_sys_net.call_args_list[0], +- ) ++ devname = random_string() ++ self.read_sys_net.side_effect = Exception("error") ++ assert "" == net.get_dev_features(devname) ++ assert 1 == self.read_sys_net.call_count ++ self.read_sys_net.assert_called_once_with(devname, "device/features") + + @mock.patch("cloudinit.net.get_dev_features") + def test_has_netfail_standby_feature(self, m_dev_features): +- devname = self.random_string() ++ devname = random_string() + standby_features = ("0" * 62) + "1" + "0" + m_dev_features.return_value = standby_features +- self.assertTrue(net.has_netfail_standby_feature(devname)) ++ assert net.has_netfail_standby_feature(devname) + + @mock.patch("cloudinit.net.get_dev_features") + def test_has_netfail_standby_feature_short_is_false(self, m_dev_features): +- devname = self.random_string() +- standby_features = self.random_string() ++ devname = random_string() ++ standby_features = random_string() + m_dev_features.return_value = standby_features +- self.assertFalse(net.has_netfail_standby_feature(devname)) ++ assert not net.has_netfail_standby_feature(devname) + + @mock.patch("cloudinit.net.get_dev_features") + def test_has_netfail_standby_feature_not_present_is_false( + self, m_dev_features + ): +- devname = self.random_string() ++ devname = random_string() + standby_features = "0" * 64 + m_dev_features.return_value = standby_features +- self.assertFalse(net.has_netfail_standby_feature(devname)) ++ assert not net.has_netfail_standby_feature(devname) + + @mock.patch("cloudinit.net.get_dev_features") + def test_has_netfail_standby_feature_no_features_is_false( + self, m_dev_features + ): +- devname = self.random_string() ++ devname = random_string() + standby_features = None + m_dev_features.return_value = standby_features +- self.assertFalse(net.has_netfail_standby_feature(devname)) ++ assert not net.has_netfail_standby_feature(devname) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_master(self, m_exists, m_standby): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" + m_exists.return_value = False # no master sysfs attr + m_standby.return_value = True # has standby feature flag +- self.assertTrue(net.is_netfail_master(devname, driver)) ++ assert net.is_netfail_master(devname, driver) + + @mock.patch("cloudinit.net.sys_dev_path") + def test_is_netfail_master_checks_master_attr(self, m_sysdev): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" +- m_sysdev.return_value = self.random_string() +- self.assertFalse(net.is_netfail_master(devname, driver)) +- self.assertEqual(1, m_sysdev.call_count) +- self.assertEqual( +- mock.call(devname, path="master"), m_sysdev.call_args_list[0] +- ) ++ m_sysdev.return_value = random_string() ++ assert not net.is_netfail_master(devname, driver) ++ assert 1 == m_sysdev.call_count ++ m_sysdev.assert_called_once_with(devname, path="master") + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_master_wrong_driver(self, m_exists, m_standby): +- devname = self.random_string() +- driver = self.random_string() +- self.assertFalse(net.is_netfail_master(devname, driver)) ++ devname = random_string() ++ driver = random_string() ++ assert not net.is_netfail_master(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_master_has_master_attr(self, m_exists, m_standby): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" + m_exists.return_value = True # has master sysfs attr +- self.assertFalse(net.is_netfail_master(devname, driver)) ++ assert not net.is_netfail_master(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_master_no_standby_feat(self, m_exists, m_standby): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" + m_exists.return_value = False # no master sysfs attr + m_standby.return_value = False # no standby feature flag +- self.assertFalse(net.is_netfail_master(devname, driver)) ++ assert not net.is_netfail_master(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + @mock.patch("cloudinit.net.sys_dev_path") + def test_is_netfail_primary(self, m_sysdev, m_exists, m_standby): +- devname = self.random_string() +- driver = self.random_string() # device not virtio_net +- master_devname = self.random_string() ++ devname = random_string() ++ driver = random_string() # device not virtio_net ++ master_devname = random_string() + m_sysdev.return_value = "%s/%s" % ( +- self.random_string(), ++ random_string(), + master_devname, + ) + m_exists.return_value = True # has master sysfs attr +- self.m_device_driver.return_value = "virtio_net" # master virtio_net ++ self.device_driver.return_value = "virtio_net" # master virtio_net + m_standby.return_value = True # has standby feature flag +- self.assertTrue(net.is_netfail_primary(devname, driver)) +- self.assertEqual(1, self.m_device_driver.call_count) +- self.assertEqual( +- mock.call(master_devname), self.m_device_driver.call_args_list[0] +- ) +- self.assertEqual(1, m_standby.call_count) +- self.assertEqual( +- mock.call(master_devname), m_standby.call_args_list[0] +- ) ++ assert net.is_netfail_primary(devname, driver) ++ self.device_driver.assert_called_once_with(master_devname) ++ assert 1 == m_standby.call_count ++ m_standby.assert_called_once_with(master_devname) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") +@@ -1588,18 +1578,18 @@ class TestNetFailOver(CiTestCase): + def test_is_netfail_primary_wrong_driver( + self, m_sysdev, m_exists, m_standby + ): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" +- self.assertFalse(net.is_netfail_primary(devname, driver)) ++ assert not net.is_netfail_primary(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + @mock.patch("cloudinit.net.sys_dev_path") + def test_is_netfail_primary_no_master(self, m_sysdev, m_exists, m_standby): +- devname = self.random_string() +- driver = self.random_string() # device not virtio_net ++ devname = random_string() ++ driver = random_string() # device not virtio_net + m_exists.return_value = False # no master sysfs attr +- self.assertFalse(net.is_netfail_primary(devname, driver)) ++ assert not net.is_netfail_primary(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") +@@ -1607,16 +1597,16 @@ class TestNetFailOver(CiTestCase): + def test_is_netfail_primary_bad_master( + self, m_sysdev, m_exists, m_standby + ): +- devname = self.random_string() +- driver = self.random_string() # device not virtio_net +- master_devname = self.random_string() ++ devname = random_string() ++ driver = random_string() # device not virtio_net ++ master_devname = random_string() + m_sysdev.return_value = "%s/%s" % ( +- self.random_string(), ++ random_string(), + master_devname, + ) + m_exists.return_value = True # has master sysfs attr +- self.m_device_driver.return_value = "XXXX" # master not virtio_net +- self.assertFalse(net.is_netfail_primary(devname, driver)) ++ self.device_driver.return_value = "XXXX" # master not virtio_net ++ assert not net.is_netfail_primary(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") +@@ -1624,77 +1614,77 @@ class TestNetFailOver(CiTestCase): + def test_is_netfail_primary_no_standby( + self, m_sysdev, m_exists, m_standby + ): +- devname = self.random_string() +- driver = self.random_string() # device not virtio_net +- master_devname = self.random_string() ++ devname = random_string() ++ driver = random_string() # device not virtio_net ++ master_devname = random_string() + m_sysdev.return_value = "%s/%s" % ( +- self.random_string(), ++ random_string(), + master_devname, + ) + m_exists.return_value = True # has master sysfs attr +- self.m_device_driver.return_value = "virtio_net" # master virtio_net ++ self.device_driver.return_value = "virtio_net" # master virtio_net + m_standby.return_value = False # master has no standby feature flag +- self.assertFalse(net.is_netfail_primary(devname, driver)) ++ assert not net.is_netfail_primary(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_standby(self, m_exists, m_standby): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" + m_exists.return_value = True # has master sysfs attr + m_standby.return_value = True # has standby feature flag +- self.assertTrue(net.is_netfail_standby(devname, driver)) ++ assert net.is_netfail_standby(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_standby_wrong_driver(self, m_exists, m_standby): +- devname = self.random_string() +- driver = self.random_string() +- self.assertFalse(net.is_netfail_standby(devname, driver)) ++ devname = random_string() ++ driver = random_string() ++ assert not net.is_netfail_standby(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_standby_no_master(self, m_exists, m_standby): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" + m_exists.return_value = False # has master sysfs attr +- self.assertFalse(net.is_netfail_standby(devname, driver)) ++ assert not net.is_netfail_standby(devname, driver) + + @mock.patch("cloudinit.net.has_netfail_standby_feature") + @mock.patch("cloudinit.net.os.path.exists") + def test_is_netfail_standby_no_standby_feature(self, m_exists, m_standby): +- devname = self.random_string() ++ devname = random_string() + driver = "virtio_net" + m_exists.return_value = True # has master sysfs attr + m_standby.return_value = False # has standby feature flag +- self.assertFalse(net.is_netfail_standby(devname, driver)) ++ assert not net.is_netfail_standby(devname, driver) + + @mock.patch("cloudinit.net.is_netfail_standby") + @mock.patch("cloudinit.net.is_netfail_primary") + def test_is_netfailover_primary(self, m_primary, m_standby): +- devname = self.random_string() +- driver = self.random_string() ++ devname = random_string() ++ driver = random_string() + m_primary.return_value = True + m_standby.return_value = False +- self.assertTrue(net.is_netfailover(devname, driver)) ++ assert net.is_netfailover(devname, driver) + + @mock.patch("cloudinit.net.is_netfail_standby") + @mock.patch("cloudinit.net.is_netfail_primary") + def test_is_netfailover_standby(self, m_primary, m_standby): +- devname = self.random_string() +- driver = self.random_string() ++ devname = random_string() ++ driver = random_string() + m_primary.return_value = False + m_standby.return_value = True +- self.assertTrue(net.is_netfailover(devname, driver)) ++ assert net.is_netfailover(devname, driver) + + @mock.patch("cloudinit.net.is_netfail_standby") + @mock.patch("cloudinit.net.is_netfail_primary") + def test_is_netfailover_returns_false(self, m_primary, m_standby): +- devname = self.random_string() +- driver = self.random_string() ++ devname = random_string() ++ driver = random_string() + m_primary.return_value = False + m_standby.return_value = False +- self.assertFalse(net.is_netfailover(devname, driver)) ++ assert not net.is_netfailover(devname, driver) + + + class TestOpenvswitchIsInstalled: +diff --git a/tests/unittests/sources/helpers/test_openstack.py b/tests/unittests/sources/helpers/test_openstack.py +index ac8e2a3..4d85ec3 100644 +--- a/tests/unittests/sources/helpers/test_openstack.py ++++ b/tests/unittests/sources/helpers/test_openstack.py +@@ -3,14 +3,13 @@ + from unittest import mock + + from cloudinit.sources.helpers import openstack +-from tests.unittests import helpers as test_helpers + + + @mock.patch( + "cloudinit.net.is_openvswitch_internal_interface", + mock.Mock(return_value=False), + ) +-class TestConvertNetJson(test_helpers.CiTestCase): ++class TestConvertNetJson: + def test_phy_types(self): + """Verify the different known physical types are handled.""" + # network_data.json example from +@@ -54,11 +53,8 @@ class TestConvertNetJson(test_helpers.CiTestCase): + + for t in openstack.KNOWN_PHYSICAL_TYPES: + net_json["links"][0]["type"] = t +- self.assertEqual( +- expected, +- openstack.convert_net_json( +- network_json=net_json, known_macs=macs +- ), ++ assert expected == openstack.convert_net_json( ++ network_json=net_json, known_macs=macs + ) + + def test_subnet_dns(self): +@@ -113,9 +109,6 @@ class TestConvertNetJson(test_helpers.CiTestCase): + + for t in openstack.KNOWN_PHYSICAL_TYPES: + net_json["links"][0]["type"] = t +- self.assertEqual( +- expected, +- openstack.convert_net_json( +- network_json=net_json, known_macs=macs +- ), ++ assert expected == openstack.convert_net_json( ++ network_json=net_json, known_macs=macs + ) +-- +2.43.0 + diff --git a/cloud-init.spec b/cloud-init.spec index ddbeffa..048c105 100644 --- a/cloud-init.spec +++ b/cloud-init.spec @@ -1,6 +1,6 @@ Name: cloud-init Version: 23.4.1 -Release: 1 +Release: 2 Summary: the defacto multi-distribution package that handles early initialization of a cloud instance. License: ASL 2.0 or GPLv3 URL: http://launchpad.net/cloud-init @@ -14,13 +14,19 @@ Patch3: add-variable-to-forbid-tmp-dir.patch Patch5: Do-not-write-NM_CONTROLLED-no-in-generated-interface-config.patch Patch6: delete-config-nopasswd-all.patch +Patch6000: backport-fix-unpin-jsonschema-and-update-tests.patch +Patch6001: backport-test-fix-tmpdir-in-test_cc_apk_configure.patch +Patch6002: backport-bug-tests-mock-reads-of-host-s-sys-class-net-via-get.patch +Patch6003: backport-tests-drop-CiTestCase-and-convert-to-pytest.patch +Patch6004: backport-test-fix-disable_sysfs_net-mock.patch + BuildRequires: pkgconfig(systemd) python3-devel python3-setuptools systemd BuildRequires: iproute python3-configobj python3-responses BuildRequires: python3-jinja2 python3-jsonpatch python3-jsonschema BuildRequires: python3-mock python3-oauthlib python3-prettytable BuildRequires: python3-pyserial python3-PyYAML python3-requests BuildRequires: dnf %{_vendor}-release python3-pytest passwd python3-netifaces -BuildRequires: python3-pytest-mock +BuildRequires: python3-pytest-mock python3-passlib Requires: e2fsprogs iproute python3-libselinux net-tools python3-policycoreutils Requires: procps python3-configobj python3-jinja2 python3-jsonpatch xfsprogs @@ -55,7 +61,31 @@ install -D -m 0644 %{SOURCE1} %{buildroot}/%{_tmpfilesdir}/%{name}.conf install -D -m 0644 tools/21-cloudinit.conf %{buildroot}/%{_sysconfdir}/rsyslog.d/21-cloudinit.conf %check -python3 -m pytest tests/unittests/ +SKIP_TESTS="" + +# 检测是否存在多个网卡的MAC地址是ee:ee:ee:ee:ee:ee +# https://docs.tigera.io/calico/latest/reference/faq#why-do-all-cali-interfaces-have-the-mac-address-eeeeeeeeeeee +MAC_ADDR="ee:ee:ee:ee:ee:ee" +interfaces=$(ls /sys/class/net) +duplicate_mac_matched_count=0 +for iface in $interfaces; do + if [ -e "/sys/class/net/$iface/address" ]; then + iface_mac=$(cat /sys/class/net/$iface/address) + if [ "$iface_mac" == "$MAC_ADDR" ]; then + duplicate_mac_matched_count=$((duplicate_mac_matched_count+1)) + fi + fi +done + +if [ "$duplicate_mac_matched_count" -gt 1 ]; then + SKIP_TESTS="not test_dhcp.py and not test_network_state.py" +fi + +if [ -n "$SKIP_TESTS" ]; then + python3 -m pytest tests/unittests/ -k "$SKIP_TESTS" +else + python3 -m pytest tests/unittests/ +fi %pre @@ -119,6 +149,18 @@ fi %exclude /usr/share/doc/* %changelog +* Wed Apr 03 2024 shixuantong - 23.4.1-2 +- Type:bugfix +- CVE:NA +- SUG:NA +- DESC:add python3-passlib to BuildRequires + skip some test if there are multiple NICs with the MAC address 'ee:ee:ee:ee:ee:ee' + fix: unpin jsonschema and update tests + test: fix tmpdir in test_cc_apk_configure + bug(tests): mock reads of host's /sys/class/net via get_sys_class_path + test: fix disable_sysfs_net mock + tests: drop CiTestCase and convert to pytest + * Wed Jan 24 2024 shixuantong - 23.4.1-1 - Type:enhancement - CVE:NA -- Gitee