From 02879c364706e2d52900558bf31e44a5360fe16b Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 12 Jun 2025 14:31:13 -0600 Subject: [PATCH 001/150] Release 25.1.3 Bump the version in cloudinit/version.py to 25.1.3 and update ChangeLog. --- ChangeLog | 8 +++++++- cloudinit/version.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index fd38caf9e63..240a3f55d16 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,10 @@ --25.1.2 +25.1.3 + - docs: provide example3 for PAM and ssh_pwauth behavior (#27) + - fix: Make hotplug socket writable only by root (#25) (CVE-2024-11584) + - fix: Don't attempt to identify non-x86 OpenStack instances (LP: #2069607) + (CVE-2024-6174) + +25.1.2 - fix: ensure MAAS datasource retries on failure (#6167) 25.1.1 diff --git a/cloudinit/version.py b/cloudinit/version.py index eb772d8f449..384a5671ceb 100644 --- a/cloudinit/version.py +++ b/cloudinit/version.py @@ -4,7 +4,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -__VERSION__ = "25.1.2" +__VERSION__ = "25.1.3" _PACKAGED_VERSION = "@@PACKAGED_VERSION@@" FEATURES = [ From 009c4cfd30aba80699b0a014ea5010ba43661d6b Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 24 Jun 2025 14:43:28 -0600 Subject: [PATCH 002/150] Release 25.1.4 Bump the version in cloudinit/version.py to 25.1.4 and update ChangeLog. --- ChangeLog | 4 ++++ cloudinit/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 240a3f55d16..9440238e223 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +25.1.4 +- fix: disable cloud-init when non-x86 environments have no DMI-data and + no strict datasources detected (LP: #2069607) (CVE-2024-6174) + 25.1.3 - docs: provide example3 for PAM and ssh_pwauth behavior (#27) - fix: Make hotplug socket writable only by root (#25) (CVE-2024-11584) diff --git a/cloudinit/version.py b/cloudinit/version.py index 384a5671ceb..b0124088283 100644 --- a/cloudinit/version.py +++ b/cloudinit/version.py @@ -4,7 +4,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -__VERSION__ = "25.1.3" +__VERSION__ = "25.1.4" _PACKAGED_VERSION = "@@PACKAGED_VERSION@@" FEATURES = [ From 6853617229a3c8009bc84479a34ef0d6a15588d3 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 26 Jun 2025 21:26:19 +0200 Subject: [PATCH 003/150] test: pytestify cloudinit.s.helpers.netlink tests (#6273) --- .../unittests/sources/helpers/test_netlink.py | 129 +++++++++--------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/tests/unittests/sources/helpers/test_netlink.py b/tests/unittests/sources/helpers/test_netlink.py index b68c8006c41..010b8d31b24 100644 --- a/tests/unittests/sources/helpers/test_netlink.py +++ b/tests/unittests/sources/helpers/test_netlink.py @@ -6,6 +6,8 @@ import socket import struct +import pytest + from cloudinit.sources.helpers.netlink import ( MAX_SIZE, OPER_DORMANT, @@ -29,7 +31,7 @@ wait_for_nic_attach_event, wait_for_nic_detach_event, ) -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock def int_to_bytes(i): @@ -39,22 +41,22 @@ def int_to_bytes(i): return codecs.decode(hex_value, "hex_codec") -class TestCreateBoundNetlinkSocket(CiTestCase): +class TestCreateBoundNetlinkSocket: @mock.patch("cloudinit.sources.helpers.netlink.socket.socket") def test_socket_error_on_create(self, m_socket): """create_bound_netlink_socket catches socket creation exception""" # NetlinkCreateSocketError is raised when socket creation errors. m_socket.side_effect = socket.error("Fake socket failure") - with self.assertRaises(NetlinkCreateSocketError) as ctx_mgr: + with pytest.raises( + NetlinkCreateSocketError, + match="Exception during netlink socket create: Fake socket" + " failure", + ): create_bound_netlink_socket() - self.assertEqual( - "Exception during netlink socket create: Fake socket failure", - str(ctx_mgr.exception), - ) -class TestReadNetlinkSocket(CiTestCase): +class TestReadNetlinkSocket: @mock.patch("cloudinit.sources.helpers.netlink.socket.socket") @mock.patch("cloudinit.sources.helpers.netlink.select.select") def test_read_netlink_socket(self, m_select, m_socket): @@ -65,8 +67,8 @@ def test_read_netlink_socket(self, m_select, m_socket): recv_data = read_netlink_socket(m_socket, 2) m_select.assert_called_with([m_socket], [], [], 2) m_socket.recv.assert_called_with(MAX_SIZE) - self.assertIsNotNone(recv_data) - self.assertEqual(recv_data, data) + assert recv_data is not None + assert recv_data == data @mock.patch("cloudinit.sources.helpers.netlink.socket.socket") @mock.patch("cloudinit.sources.helpers.netlink.select.select") @@ -75,18 +77,17 @@ def test_netlink_read_timeout(self, m_select, m_socket): m_select.return_value = [], None, None data = read_netlink_socket(m_socket, 1) m_select.assert_called_with([m_socket], [], [], 1) - self.assertEqual(m_socket.recv.call_count, 0) - self.assertIsNone(data) + assert m_socket.recv.call_count == 0 + assert data is None def test_read_invalid_socket(self): """read_netlink_socket raises assert error if socket is invalid""" socket = None - with self.assertRaises(AssertionError) as context: + with pytest.raises(AssertionError, match="netlink socket is none"): read_netlink_socket(socket, 1) - self.assertTrue("netlink socket is none" in str(context.exception)) -class TestParseNetlinkMessage(CiTestCase): +class TestParseNetlinkMessage: def test_read_rta_oper_state(self): """read_rta_oper_state could parse netlink message and extract data""" ifname = "eth0" @@ -104,15 +105,14 @@ def test_read_rta_oper_state(self): int_to_bytes(OPER_DOWN), ) interface_state = read_rta_oper_state(buf) - self.assertEqual(interface_state.ifname, ifname) - self.assertEqual(interface_state.operstate, OPER_DOWN) + assert interface_state.ifname == ifname + assert interface_state.operstate == OPER_DOWN def test_read_none_data(self): """read_rta_oper_state raises assert error if data is none""" data = None - with self.assertRaises(AssertionError) as context: + with pytest.raises(AssertionError, match="data is none"): read_rta_oper_state(data) - self.assertEqual("data is none", str(context.exception)) def test_read_invalid_rta_operstate_none(self): """read_rta_oper_state returns none if operstate is none""" @@ -121,7 +121,7 @@ def test_read_invalid_rta_operstate_none(self): bytes = ifname.encode("utf-8") struct.pack_into("HH4s", buf, RTATTR_START_OFFSET, 8, 3, bytes) interface_state = read_rta_oper_state(buf) - self.assertIsNone(interface_state) + assert interface_state is None def test_read_invalid_rta_ifname_none(self): """read_rta_oper_state returns none if ifname is none""" @@ -130,41 +130,37 @@ def test_read_invalid_rta_ifname_none(self): "HHc", buf, RTATTR_START_OFFSET, 5, 16, int_to_bytes(OPER_DOWN) ) interface_state = read_rta_oper_state(buf) - self.assertIsNone(interface_state) + assert interface_state is None def test_read_invalid_data_len(self): """raise assert error if data size is smaller than required size""" buf = bytearray(32) - with self.assertRaises(AssertionError) as context: + with pytest.raises( + AssertionError, + match="length of data is smaller than RTATTR_START_OFFSET", + ): read_rta_oper_state(buf) - self.assertTrue( - "length of data is smaller than RTATTR_START_OFFSET" - in str(context.exception) - ) def test_unpack_rta_attr_none_data(self): """unpack_rta_attr raises assert error if data is none""" data = None - with self.assertRaises(AssertionError) as context: + with pytest.raises(AssertionError, match="data is none"): unpack_rta_attr(data, RTATTR_START_OFFSET) - self.assertTrue("data is none" in str(context.exception)) def test_unpack_rta_attr_invalid_offset(self): """unpack_rta_attr raises assert error if offset is invalid""" data = bytearray(48) - with self.assertRaises(AssertionError) as context: + with pytest.raises(AssertionError, match="offset is not integer"): unpack_rta_attr(data, "offset") - self.assertTrue("offset is not integer" in str(context.exception)) - with self.assertRaises(AssertionError) as context: + with pytest.raises( + AssertionError, match="rta offset is less than expected length" + ): unpack_rta_attr(data, 31) - self.assertTrue( - "rta offset is less than expected length" in str(context.exception) - ) @mock.patch("cloudinit.sources.helpers.netlink.socket.socket") @mock.patch("cloudinit.sources.helpers.netlink.read_netlink_socket") -class TestNicAttachDetach(CiTestCase): +class TestNicAttachDetach: with_logs = True def _media_switch_data(self, ifname, msg_type, operstate): @@ -206,8 +202,8 @@ def test_nic_attached_oper_down(self, m_read_netlink_socket, m_socket): data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN) m_read_netlink_socket.side_effect = [data_op_down] ifread = wait_for_nic_attach_event(m_socket, []) - self.assertEqual(m_read_netlink_socket.call_count, 1) - self.assertEqual(ifname, ifread) + assert m_read_netlink_socket.call_count == 1 + assert ifname == ifread def test_nic_attached_oper_up(self, m_read_netlink_socket, m_socket): """Test for a new nic attached""" @@ -215,8 +211,8 @@ def test_nic_attached_oper_up(self, m_read_netlink_socket, m_socket): data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP) m_read_netlink_socket.side_effect = [data_op_up] ifread = wait_for_nic_attach_event(m_socket, []) - self.assertEqual(m_read_netlink_socket.call_count, 1) - self.assertEqual(ifname, ifread) + assert m_read_netlink_socket.call_count == 1 + assert ifname == ifread def test_nic_attach_ignore_existing(self, m_read_netlink_socket, m_socket): """Test that we read only the interfaces we are interested in.""" @@ -224,8 +220,8 @@ def test_nic_attach_ignore_existing(self, m_read_netlink_socket, m_socket): data_eth1 = self._media_switch_data("eth1", RTM_NEWLINK, OPER_DOWN) m_read_netlink_socket.side_effect = [data_eth0, data_eth1] ifread = wait_for_nic_attach_event(m_socket, ["eth0"]) - self.assertEqual(m_read_netlink_socket.call_count, 2) - self.assertEqual("eth1", ifread) + assert m_read_netlink_socket.call_count == 2 + assert "eth1" == ifread def test_nic_attach_read_first(self, m_read_netlink_socket, m_socket): """Test that we read only the interfaces we are interested in.""" @@ -233,8 +229,8 @@ def test_nic_attach_read_first(self, m_read_netlink_socket, m_socket): data_eth1 = self._media_switch_data("eth1", RTM_NEWLINK, OPER_DOWN) m_read_netlink_socket.side_effect = [data_eth0, data_eth1] ifread = wait_for_nic_attach_event(m_socket, ["eth1"]) - self.assertEqual(m_read_netlink_socket.call_count, 1) - self.assertEqual("eth0", ifread) + assert m_read_netlink_socket.call_count == 1 + assert "eth0" == ifread def test_nic_detached(self, m_read_netlink_socket, m_socket): """Test for an existing nic detached""" @@ -242,13 +238,13 @@ def test_nic_detached(self, m_read_netlink_socket, m_socket): data_op_down = self._media_switch_data(ifname, RTM_DELLINK, OPER_DOWN) m_read_netlink_socket.side_effect = [data_op_down] ifread = wait_for_nic_detach_event(m_socket) - self.assertEqual(m_read_netlink_socket.call_count, 1) - self.assertEqual(ifname, ifread) + assert m_read_netlink_socket.call_count == 1 + assert ifname == ifread @mock.patch("cloudinit.sources.helpers.netlink.socket.socket") @mock.patch("cloudinit.sources.helpers.netlink.read_netlink_socket") -class TestWaitForMediaDisconnectConnect(CiTestCase): +class TestWaitForMediaDisconnectConnect: with_logs = True def _media_switch_data(self, ifname, msg_type, operstate): @@ -293,10 +289,10 @@ def test_media_down_up_scenario(self, m_read_netlink_socket, m_socket): data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP) m_read_netlink_socket.side_effect = [data_op_down, data_op_up] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 2) + assert m_read_netlink_socket.call_count == 2 def test_wait_for_media_switch_diff_interface( - self, m_read_netlink_socket, m_socket + self, m_read_netlink_socket, m_socket, caplog ): """wait_for_media_disconnect_connect ignores unexpected interfaces. @@ -326,11 +322,11 @@ def test_wait_for_media_switch_diff_interface( data_op_up_eth0, ] wait_for_media_disconnect_connect(m_socket, expected_ifname) - self.assertIn( - "Ignored netlink event on interface %s" % other_ifname, - self.logs.getvalue(), + assert ( + "Ignored netlink event on interface %s" % other_ifname + in caplog.text ) - self.assertEqual(m_read_netlink_socket.call_count, 4) + assert m_read_netlink_socket.call_count == 4 def test_invalid_msgtype_getlink(self, m_read_netlink_socket, m_socket): """wait_for_media_disconnect_connect ignores GETLINK events. @@ -357,7 +353,7 @@ def test_invalid_msgtype_getlink(self, m_read_netlink_socket, m_socket): data_newlink_up, ] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 4) + assert m_read_netlink_socket.call_count == 4 def test_invalid_msgtype_setlink(self, m_read_netlink_socket, m_socket): """wait_for_media_disconnect_connect ignores SETLINK events. @@ -387,7 +383,7 @@ def test_invalid_msgtype_setlink(self, m_read_netlink_socket, m_socket): data_newlink_up, ] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 4) + assert m_read_netlink_socket.call_count == 4 def test_netlink_invalid_switch_scenario( self, m_read_netlink_socket, m_socket @@ -428,7 +424,7 @@ def test_netlink_invalid_switch_scenario( data_op_up, ] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 14) + assert m_read_netlink_socket.call_count == 14 def test_netlink_valid_inbetween_transitions( self, m_read_netlink_socket, m_socket @@ -450,7 +446,7 @@ def test_netlink_valid_inbetween_transitions( data_op_up, ] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 4) + assert m_read_netlink_socket.call_count == 4 def test_netlink_invalid_operstate(self, m_read_netlink_socket, m_socket): """wait_for_media_disconnect_connect should handle invalid operstates. @@ -470,28 +466,25 @@ def test_netlink_invalid_operstate(self, m_read_netlink_socket, m_socket): data_op_up, ] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 5) + assert m_read_netlink_socket.call_count == 5 def test_wait_invalid_socket(self, m_read_netlink_socket, m_socket): """wait_for_media_disconnect_connect handle none netlink socket.""" socket = None ifname = "eth0" - with self.assertRaises(AssertionError) as context: + with pytest.raises(AssertionError, match="netlink socket is none"): wait_for_media_disconnect_connect(socket, ifname) - self.assertTrue("netlink socket is none" in str(context.exception)) def test_wait_invalid_ifname(self, m_read_netlink_socket, m_socket): """wait_for_media_disconnect_connect handle none interface name""" ifname = None - with self.assertRaises(AssertionError) as context: + with pytest.raises(AssertionError, match="interface name is none"): wait_for_media_disconnect_connect(m_socket, ifname) - self.assertTrue("interface name is none" in str(context.exception)) ifname = "" - with self.assertRaises(AssertionError) as context: + with pytest.raises( + AssertionError, match="interface name cannot be empty" + ): wait_for_media_disconnect_connect(m_socket, ifname) - self.assertTrue( - "interface name cannot be empty" in str(context.exception) - ) def test_wait_invalid_rta_attr(self, m_read_netlink_socket, m_socket): """wait_for_media_disconnect_connect handles invalid rta data""" @@ -507,7 +500,7 @@ def test_wait_invalid_rta_attr(self, m_read_netlink_socket, m_socket): data_op_up, ] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 4) + assert m_read_netlink_socket.call_count == 4 def test_read_multiple_netlink_msgs(self, m_read_netlink_socket, m_socket): """Read multiple messages in single receive call""" @@ -540,7 +533,7 @@ def test_read_multiple_netlink_msgs(self, m_read_netlink_socket, m_socket): ) m_read_netlink_socket.return_value = data wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 1) + assert m_read_netlink_socket.call_count == 1 def test_read_partial_netlink_msgs(self, m_read_netlink_socket, m_socket): """Read partial messages in receive call""" @@ -570,4 +563,4 @@ def test_read_partial_netlink_msgs(self, m_read_netlink_socket, m_socket): ) m_read_netlink_socket.side_effect = [data1, data2] wait_for_media_disconnect_connect(m_socket, ifname) - self.assertEqual(m_read_netlink_socket.call_count, 2) + assert m_read_netlink_socket.call_count == 2 From 0ecd7b77204347d05f44f829c1c39ec8cd9d9c6b Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Mon, 30 Jun 2025 22:23:57 +0200 Subject: [PATCH 004/150] feat(hetzner): integrate private networks metadata (#6224) Signed-off-by: Thomas Boerger --- cloudinit/sources/DataSourceHetzner.py | 7 +++++++ cloudinit/sources/helpers/hetzner.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cloudinit/sources/DataSourceHetzner.py b/cloudinit/sources/DataSourceHetzner.py index 7b919f66fa4..53a950f8e94 100644 --- a/cloudinit/sources/DataSourceHetzner.py +++ b/cloudinit/sources/DataSourceHetzner.py @@ -78,6 +78,12 @@ def _get_data(self): sec_between=self.wait_retry, retries=self.retries, ) + pn = hc_helper.read_metadata( + self.metadata_address + "/private-networks", + timeout=self.timeout, + sec_between=self.wait_retry, + retries=self.retries, + ) except NoDHCPLeaseError as e: LOG.error("Bailing, DHCP Exception: %s", e) raise @@ -99,6 +105,7 @@ def _get_data(self): self.metadata["local-hostname"] = md["hostname"] self.metadata["network-config"] = md.get("network-config", None) self.metadata["public-keys"] = md.get("public-keys", None) + self.metadata["private-networks"] = pn self.vendordata_raw = md.get("vendor_data", None) # instance-id and serial from SMBIOS should be identical diff --git a/cloudinit/sources/helpers/hetzner.py b/cloudinit/sources/helpers/hetzner.py index a1fd92ff37d..50fbcb0468a 100644 --- a/cloudinit/sources/helpers/hetzner.py +++ b/cloudinit/sources/helpers/hetzner.py @@ -12,7 +12,7 @@ def read_metadata(url, timeout=2, sec_between=2, retries=30): ) if not response.ok(): raise RuntimeError("unable to read metadata at %s" % url) - return util.load_yaml(response.contents.decode()) + return util.load_yaml(response.contents.decode(), allowed=(dict, list)) def read_userdata(url, timeout=2, sec_between=2, retries=30): From 170ef23a8d6765f66b80a6415309fbdb3122550d Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 30 Jun 2025 22:39:58 +0200 Subject: [PATCH 005/150] test: pytestify t/u/test_log.py (#6275) --- tests/unittests/test_log.py | 57 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/tests/unittests/test_log.py b/tests/unittests/test_log.py index 6a71c703bec..4b1998fb4a0 100644 --- a/tests/unittests/test_log.py +++ b/tests/unittests/test_log.py @@ -13,25 +13,32 @@ from cloudinit import lifecycle, util from cloudinit.analyze.dump import CLOUD_INIT_ASCTIME_FMT from cloudinit.log import loggers -from tests.unittests.helpers import CiTestCase - - -class TestCloudInitLogger(CiTestCase): - def setUp(self): - # set up a logger like cloud-init does in setup_logging, but instead - # of sys.stderr, we'll plug in a StringIO() object so we can see - # what gets logged - logging.Formatter.converter = time.gmtime - self.ci_logs = io.StringIO() - self.ci_root = logging.getLogger() - console = logging.StreamHandler(self.ci_logs) - console.setFormatter(logging.Formatter(loggers.DEFAULT_LOG_FORMAT)) - console.setLevel(logging.DEBUG) - self.ci_root.addHandler(console) - self.ci_root.setLevel(logging.DEBUG) - self.LOG = logging.getLogger("test_cloudinit_logger") - - def test_logger_uses_gmtime(self): + + +@pytest.fixture +def ci_logs(): + return io.StringIO() + + +@pytest.fixture +def log(ci_logs): + # set up a logger like cloud-init does in setup_logging, but instead + # of sys.stderr, we'll plug in a StringIO() object so we can see + # what gets logged + logging.Formatter.converter = time.gmtime + ci_root = logging.getLogger() + console = logging.StreamHandler(ci_logs) + console.setFormatter(logging.Formatter(loggers.DEFAULT_LOG_FORMAT)) + console.setLevel(logging.DEBUG) + ci_root.addHandler(console) + ci_root.setLevel(logging.DEBUG) + LOG = logging.getLogger("test_cloudinit_logger") + return LOG + + +class TestCloudInitLogger: + + def test_logger_uses_gmtime(self, log, ci_logs): """Test that log message have timestamp in UTC (gmtime)""" # Log a message, extract the timestamp from the log entry @@ -56,7 +63,7 @@ def remove_tz(_dt: datetime.datetime) -> datetime.datetime: datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(0, 0.5) ) - self.LOG.error("Test message") + log.error("Test message") utc_after = remove_tz( datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(0, 0.5) @@ -64,16 +71,16 @@ def remove_tz(_dt: datetime.datetime) -> datetime.datetime: # extract timestamp from log: # 2017-08-23 14:19:43,069 - test_log.py[ERROR]: Test message - logstr = self.ci_logs.getvalue().splitlines()[0] + logstr = ci_logs.getvalue().splitlines()[0] timestampstr = logstr.split(" - ")[0] parsed_dt = datetime.datetime.strptime( timestampstr, CLOUD_INIT_ASCTIME_FMT ) - self.assertLess(utc_before, parsed_dt) - self.assertLess(parsed_dt, utc_after) - self.assertLess(utc_before, utc_after) - self.assertGreater(utc_after, parsed_dt) + assert utc_before < parsed_dt + assert parsed_dt < utc_after + assert utc_before < utc_after + assert utc_after > parsed_dt class TestDeprecatedLogs: From bffd332092534ffcc9e3d4d95081ee341a89f794 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 30 Jun 2025 22:53:53 +0200 Subject: [PATCH 006/150] test: pytestify ds_identify tests (#6274) --- tests/unittests/test_ds_identify.py | 1123 ++++++++++++++------------- 1 file changed, 572 insertions(+), 551 deletions(-) diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 00aa1e730ca..63d415b6384 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -18,7 +18,6 @@ from cloudinit.sources import DataSourceSmartOS as ds_smartos from tests.helpers import cloud_init_project_dir from tests.unittests.helpers import ( - CiTestCase, dir2dict, populate_dir, populate_dir_with_ts, @@ -304,16 +303,15 @@ ) -class DsIdentifyBase(CiTestCase): +class DsIdentifyBase: dsid_path = cloud_init_project_dir("tools/ds-identify") - allowed_subp = ["sh"] # set to true to write out the mocked ds-identify for inspection debug_mode = True def call( self, - rootd=None, + rootd, mocks=None, no_mocks=None, func="main", @@ -336,11 +334,8 @@ def call( if cloudcfg not in files: files[cloudcfg] = DEFAULT_CLOUD_CONFIG - if rootd is None: - rootd = self.tmp_dir() - unset = "_unset" - wrap = self.tmp_path(path="_shwrap", dir=rootd) + wrap = os.path.join(rootd, "_shwrap") populate_dir(rootd, files) # DI_DEFAULT_POLICY* are declared always as to not rely @@ -450,7 +445,7 @@ def write_mock(data): return CallReturn(rc, out, err, cfg, dir2dict(rootd)) - def _call_via_dict(self, data, rootd=None, **kwargs): + def _call_via_dict(self, data, rootd, **kwargs): # return output of self.call with a dict input like VALID_CFG[item] xwargs = {"rootd": rootd} passthrough = ( @@ -470,25 +465,25 @@ def _call_via_dict(self, data, rootd=None, **kwargs): xwargs[k] = kwargs[k] return self.call(**xwargs) - def _test_ds_found(self, name): + def _test_ds_found(self, name, rootd): data = copy.deepcopy(VALID_CFG[name]) dslist = [] for ds in data.pop("ds").split(","): dslist.append(ds.strip()) dslist.append(DS_NONE) - return self._check_via_dict(data, RC_FOUND, dslist=dslist) + return self._check_via_dict(data, rootd, RC_FOUND, dslist=dslist) - def _test_ds_not_found(self, name): + def _test_ds_not_found(self, name, rootd): data = copy.deepcopy(VALID_CFG[name]) - return self._check_via_dict(data, RC_NOT_FOUND) + return self._check_via_dict(data, rootd, RC_NOT_FOUND) - def _check_via_dict(self, data, rc, dslist=None, **kwargs): - ret = self._call_via_dict(data, **kwargs) + def _check_via_dict(self, data, rootd, rc, dslist=None, **kwargs): + ret = self._call_via_dict(data, rootd, **kwargs) good = False try: - self.assertEqual(rc, ret.rc) + assert rc == ret.rc if dslist is not None: - self.assertEqual(dslist, ret.cfg.get("datasource_list")) + assert dslist == ret.cfg.get("datasource_list") good = True finally: if not good: @@ -498,11 +493,12 @@ def _check_via_dict(self, data, rc, dslist=None, **kwargs): return ret +@pytest.mark.allow_subp_for("sh") class TestDsIdentify(DsIdentifyBase): - def test_wb_print_variables(self): + def test_wb_print_variables(self, tmp_path): """_print_info reports an array of discovered variables to stderr.""" data = VALID_CFG["Azure-dmi-detection"] - _, _, err, _, _ = self._call_via_dict(data) + _, _, err, _, _ = self._call_via_dict(data, str(tmp_path)) expected_vars = [ "DMI_PRODUCT_NAME", "DMI_SYS_VENDOR", @@ -524,166 +520,405 @@ def test_wb_print_variables(self): "ON_NOTFOUND", ] for var in expected_vars: - self.assertIn("{0}=".format(var), err) - - def test_maas_not_detected_2(self): - """Don't incorrectly identify maas - - The bug reported in 4794 combined with the previously existing bug - reported in 4796 made for very loose MAAS false-positives. + assert "{0}=".format(var) in err - In ds-identify the function check_config() attempts to parse yaml keys - in bash, but it sometimes introduces false positives. The maas - datasource uses check_config() and the existence of a "MAAS" key to - identify itself (which is a very poor identifier - clouds should have - stricter identifiers). Since the MAAS datasource is at the begining of - the list, this is particularly troublesome and more concerning than - NoCloud false positives, for example. - """ - config = "LXD-kvm-not-MAAS-2" - self._test_ds_found(config) + @pytest.mark.parametrize( + "config,found", + [ + # Don't incorrectly identify maas + # + # The bug reported in 4794 combined with the previously existing + # bug reported in 4796 made for very loose MAAS false-positives. + # + # In ds-identify the function check_config() attempts to parse yaml + # keys in bash, but it sometimes introduces false positives. The + # maas datasource uses check_config() and the existence of a "MAAS" + # key to identify itself (which is a very poor identifier - clouds + # should have stricter identifiers). Since the MAAS datasource is + # at the begining of the list, this is particularly troublesome and + # more concerning than NoCloud false positives, for example. + pytest.param("LXD-kvm-not-MAAS-2", True, id="mass_not_detected_2"), + # Don't detect incorrect config when invalid datasource_list + # provided + # + # If unparsable list is provided we just ignore it. Some users + # might assume that since the rest of the configuration is yaml + # that multi-line yaml lists are valid (they aren't). When this + # happens, just run ds-identify and figure it out for ourselves + # which platform to run. + pytest.param( + "Azure-parse-invalid", True, id="azure_invalid_configuration" + ), + # Azure datasource is detected from DMI chassis-asset-tag + pytest.param( + "Azure-dmi-detection", + True, + id="azure_dmi_detection_from_chassis_asset_tag", + ), + # Azure datasource is detected due to presence of a seed file. + # + # The seed file tested is /var/lib/cloud/seed/azure/ovf-env.xml. + pytest.param( + "Azure-seed-detection", True, id="azure_seed_file_detection" + ), + # EC2: hvm instances use dmi serial and uuid starting with 'ec2'. + pytest.param("Ec2-hvm", True, id="aws_ec2_hvm"), + # EC2: hvm instances use dmi serial and uuid starting with 'ec2' + # + # test using SYSTEMD_VIRTUALIZATION, not systemd-detect-virt + pytest.param("Ec2-hvm-env", True, id="aws_ec2_hvm_env"), + # EC2: hvm instances use system-uuid and may have swapped + # endianness + # + # test using SYSTEMD_VIRTUALIZATION, not systemd-detect-virt + pytest.param( + "Ec2-hvm-swap-endianness", True, id="aws_ec2_hvm_endian" + ), + # EC2: sys/hypervisor/uuid starts with ec2. + pytest.param("Ec2-xen", True, id="aws_ec2_xen"), + # EC2: product_serial ends with '.brightbox.com' + pytest.param("Ec2-brightbox", True, id="brightbox_is_ec2"), + # EC2: bobrightbox.com in product_serial is not brightbox + pytest.param( + "Ec2-brightbox-negative", + False, + id="brightbox_is_not_brightbox", + ), + # NoCloud identified on FreeBSD via label by geom. + pytest.param("NoCloud-fbsd", True, id="freebsd_nocloud"), + # GCE identifies itself with product_name. + pytest.param("GCE", True, id="gce_by_product_name"), + # GCE identifies itself with product_name. + # + # Uses SYSTEMD_VIRTUALIZATION + pytest.param("GCE_ENV", True, id="gce_by_product_name_env"), + # Older gce compute instances must be identified by serial. + pytest.param("GCE-serial", True, id="gce_by_serial"), + # LXD KVM has race on absent /dev/lxd/socket. Use DMI board_name. + pytest.param("LXD-kvm", True, id="lxd_kvm"), + # LXD KVM on host systems with a kernel > 5.10 need to match "qemu" + # + # LXD provides `hv_passthrough` when launching kvm instances when + # host kernel is > 5.10. This results in systemd being unable to + # detect the virtualized CPUID="Linux KVM Hv" as type "kvm" and + # results in systemd-detect-virt returning "qemu" in this case. + # + # Assert ds-identify can match systemd-detect-virt="qemu" and + # /sys/class/dmi/id/board_name = LXD. + # Once systemd 251 is available on a target distro, the virtualized + # CPUID will be represented properly as "kvm" + pytest.param( + "LXD-kvm-qemu-kernel-gt-5.10", True, id="lxd_kvm_jammy" + ), + # LXD KVM on host systems with a kernel > 5.10 need to match "qemu" + # + # LXD provides `hv_passthrough` when launching kvm instances when + # host kernel is > 5.10. This results in systemd being unable to + # detect the virtualized CPUID="Linux KVM Hv" as type "kvm" and + # results in systemd-detect-virt returning "qemu" in this case. + # + # Assert ds-identify can match systemd-detect-virt="qemu" and + # /sys/class/dmi/id/board_name = LXD. + # Once systemd 251 is available on a target distro, the virtualized + # CPUID will be represented properly as "kvm" + pytest.param( + "LXD-kvm-qemu-kernel-gt-5.10-env", True, id="lxd_kvm_jammy_env" + ), + # LXD containers will have /dev/lxd/socket at generator time. + pytest.param("LXD", True, id="lxd_containers"), + # ConfigDrive datasource has a disk with LABEL=config-2. + pytest.param("ConfigDrive", True, id="config_drive"), + # Rbx datasource has a disk with LABEL=CLOUDMD. + pytest.param("RbxCloud", True, id="rbx_cloud"), + # Rbx datasource has a disk with LABEL=cloudmd. + pytest.param("RbxCloudLower", True, id="rbx_cloud_lower"), + # ConfigDrive datasource has a disk with LABEL=CONFIG-2. + pytest.param("ConfigDriveUpper", True, id="config_drive_upper"), + # Config Drive seed directory. + pytest.param("ConfigDrive-seed", True, id="config_drive_seed"), + # Multi-line yaml is unsupported + pytest.param( + "LXD-kvm-not-azure", + True, + marks=[ + pytest.mark.xfail( + reason=( + "not supported: yaml parser implemented in POSIX" + " shell" + ) + ) + ], + id="multiline_yaml", + ), + # Template provisioned with user-data first boot. + # + # Template provisioning with user-data has METADATA disk. + # datasource should return found. + pytest.param( + "IBMCloud-metadata", True, id="ibmcloud_template_userdata" + ), + # Launched by os code always has config-2 disk. + pytest.param("IBMCloud-config-2", True, id="ibmcloud_os_code"), + # Test that Aliyun cloud is identified by product id. + pytest.param("AliYun", True, id="ibmcloud_os_code"), + # On Intel, openstack must be identified. + pytest.param( + "OpenStack", True, id="default_openstack_intel_is_found" + ), + # Open Telecom identification. + pytest.param( + "OpenStack-OpenTelekom", + True, + id="openstack_open_telekom_cloud", + ), + # SAP Converged Cloud identification + pytest.param( + "OpenStack-SAPCCloud", True, id="openstack_sap_ccloud" + ), + pytest.param( + "OpenStack-SAPCCloud-env", True, id="openstack_sap_ccloud-env" + ), + # Open Huawei Cloud identification. + pytest.param( + "OpenStack-HuaweiCloud", True, id="openstack_huawei_cloud" + ), + # Open Samsung Cloud Platform identification. + pytest.param( + "OpenStack-SamsungCloudPlatform", + True, + id="openstack_samsung_cloud_platform", + ), + # OpenStack identification via asset tag OpenStack Nova. + pytest.param( + "OpenStack-AssetTag-Nova", True, id="openstack_asset_tag_nova" + ), + # OpenStack identification via asset tag OpenStack Compute. + pytest.param( + "OpenStack-AssetTag-Compute", + True, + id="openstack_asset_tag_compute", + ), + # OVF is identified found when ovf/ovf-env.xml seed file exists. + pytest.param("OVF-seed", True, id="default_ovf_is_found"), + # OVF is identified when iso9660 cdrom path contains ovf schema. + pytest.param( + "OVF", + True, + id="ovf_on_vmware_iso_found_by_cdrom_with_ovf_schema_match", + ), + # OVF guest info is found on vmware. + pytest.param( + "OVF-guestinfo", True, id="ovf_on_vmware_guestinfo_found" + ), + # NoCloud is found with iso9660 filesystem on non-cdrom disk. + pytest.param("NoCloud", True, id="default_nocloud_as_vdb_iso9660"), + # NoCloud is found with uppercase filesystem label. + pytest.param("NoCloudUpper", True, id="nocloud_upper"), + # NoCloud seed definition can go in /etc/cloud/cloud.cfg[.d] + pytest.param("NoCloud-cfg", True, id="nocloud_seed_in_cfg"), + # NoCloud fatboot label - LP: #184166. + pytest.param("NoCloud-fatboot", True, id="nocloud_fatboot"), + # Nocloud seed directory. + pytest.param("NoCloud-seed", True, id="nocloud_seed"), + # Nocloud seed directory ubuntu core writable + pytest.param( + "NoCloud-seed-ubuntu-core", + True, + id="nocloud_seed_ubuntu_core_writable", + ), + # Hetzner cloud is identified in sys_vendor. + pytest.param("Hetzner", True, id="hetzner_found"), + # CloudCIX cloud is identified in dmi product-name + pytest.param("CloudCIX", True, id="cloudcix_found"), + # NWCS is identified in sys_vendor. + pytest.param("NWCS", True, id="nwcs_found"), + # SmartOS cloud identified by SmartDC in dmi. + pytest.param("SmartOS-bhyve", True, id="smartos_bhyve"), + # SmartOS cloud identified on lxbrand container. + pytest.param("SmartOS-lxbrand", True, id="smartos_lxbrand"), + pytest.param( + "SmartOS-lxbrand-env", True, id="smartos_lxbrand-env" + ), + # EC2: chassis asset tag ends with 'zstack.io' + pytest.param("Ec2-ZStack", True, id="zstack_is_ec2"), + # EC2: e24cloud identified by sys_vendor + pytest.param("Ec2-E24Cloud", True, id="e24cloud_is_ec2"), + # EC2: bobrightbox.com in product_serial is not brightbox' + pytest.param( + "Ec2-E24Cloud-negative", False, id="e24cloud_not_active" + ), + # EC2: outscale identified by sys_vendor and product_name + pytest.param("Ec2-Outscale", True, id="outscale_is_ec2"), + # EC2: outscale in sys_vendor is not outscale' + pytest.param( + "Ec2-Outscale-negative-sysvendor", + False, + id="outscale_not_active_sysvendor", + ), + # EC2: outscale in product_name is not outscale' + pytest.param( + "Ec2-Outscale-negative-productname", + False, + id="outscale_not_active_productname", + ), + # VMware: no valid transports + pytest.param( + "VMware-NoValidTransports", + False, + id="vmware_no_valid_transports", + ), + # VMware is identified when vmware customization is enabled. + pytest.param( + "VMware-vmware-customization", + True, + id="vmware_on_vmware_when_vmware_customization_is_enabled", + ), + # VMware and OVF are identified when: + # 1. On VMware platform. + # 2. VMware customization is enabled. + # 3. iso9660 cdrom path contains ovf schema. + pytest.param( + "VMware-OVF-on-vmware-with-vmware-customization-and-ovf-schema", + True, + id="vmware_ovf_on_vmware_with_vmware_customization_and_ovf_" + "schema", + ), + # OVF is identified when: + # 1. Not on VMware platform. + # 2. VMware customization is enabled. + # 3. iso9660 cdrom path contains ovf schema. + pytest.param( + "OVF-not-on-vmware-with-vmware-customization-and-ovf-schema", + True, + id="ovf_not_on_vmware_with_vmware_customization_and_ovf_" + "schema", + ), + # VMware: envvar transport no data + pytest.param( + "VMware-EnvVar-NoData", False, id="vmware_envvar_no_data" + ), + # VMware: envvar transport success if no virt id + pytest.param( + "VMware-EnvVar-NoVirtID", True, id="vmware_envvar_no_virt_id" + ), + # VMware: envvar transport activated by metadata + pytest.param( + "VMware-EnvVar-Metadata", + True, + id="vmware_envvar_activated_by_metadata", + ), + # VMware: envvar transport activated by userdata + pytest.param( + "VMware-EnvVar-Userdata", + True, + id="vmware_envvar_activated_by_userdata", + ), + # VMware: envvar transport activated by vendordata + pytest.param( + "VMware-EnvVar-Vendordata", + True, + id="vmware_envvar_activated_by_vendordata", + ), + # VMware: guestinfo transport no data + pytest.param( + "VMware-GuestInfo-NoData-Rpctool", + False, + id="vmware_guestinfo_no_data_rcptool", + ), + pytest.param( + "VMware-GuestInfo-NoData-Vmtoolsd", + False, + id="vmware_guestinfo_no_data_vmtoolsd", + ), + # VMware: guestinfo transport fails if no virt id + pytest.param( + "VMware-GuestInfo-NoVirtID", + False, + id="vmware_guestinfo_no_virt_id", + ), + # VMware: guestinfo transport activated by metadata + pytest.param( + "VMware-GuestInfo-Metadata", + True, + id="vmware_guestinfo_activated_by_metadata", + ), + # VMware: guestinfo transport activated by userdata + pytest.param( + "VMware-GuestInfo-Userdata", + True, + id="vmware_guestinfo_activated_by_userdata", + ), + # VMware: guestinfo transport activated by vendordata + pytest.param( + "VMware-GuestInfo-Vendordata", + True, + id="vmware_guestinfo_activated_by_vendordata", + ), + # VMware and OVF are identified when: + # 1. On VMware platform. + # 2. guestinfo transport activated by metadata + # 3. iso9660 cdrom path contains ovf schema. + pytest.param( + "VMware-OVF-on-vmware-with-guestinfo-metadata-and-ovf-schema", + True, + id="vmware_ovf_on_vmware_with_guestinfo_metadata_and_ovf_" + "schema", + ), + # OVF is identified when: + # 1. Not on VMware platform. + # 2. guestinfo transport activated by metadata + # 3. iso9660 cdrom path contains ovf schema. + pytest.param( + "OVF-not-on-vmware-with-guestinfo-metadata-and-ovf-schema", + True, + id="ovf_not_on_vmware_with_guestinfo_metadata_and_ovf_schema", + ), + # ds-identify finds Akamai by system-manufacturer dmi field + pytest.param("Akamai", True, id="akamai_found_by_sys_vendor"), + # Test *BSD code paths + # + # FreeBSD doesn't have /sys so we use kenv(1) here. + # OpenBSD uses sysctl(8). + # Other BSD systems fallback to dmidecode(8). + # BSDs also doesn't have systemd-detect-virt(8), so we use + # sysctl(8) to query kern.vm_guest, and optionally map it: + # + # Test that kenv(1) works on systems which don't have /sys + pytest.param("Hetzner-kenv", True, id="bsd_dmi_kenv"), + # Test that sysctl(8) works on systems which don't have /sys + pytest.param("Hetzner-sysctl", True, id="bsd_dmi_sysctl"), + # Test that dmidecode(8) works on systems which don't have /sys + pytest.param("Hetzner-dmidecode", True, id="bsd_dmi_dmidecode"), + # Simple positive test of Oracle by chassis id. + pytest.param("Oracle", True, id="oracle_found_by_chassis"), + # Simple negative test for WSL due other virt. + pytest.param("Not-WSL", False, id="wsl_not_found_virt"), + # Negative test by lack of host filesystem mount points. + pytest.param("WSL-no-host-mounts", False, id="wsl_no_fs_mounts"), + ], + ) + def test_ds_found_not_found(self, config, found, tmp_path): + test_func = self._test_ds_found if found else self._test_ds_not_found + test_func(config, str(tmp_path)) - def test_flow_sequence_control(self): + def test_flow_sequence_control(self, tmp_path): """ensure that an invalid key in the flow_sequence tests produces no datasource list match control test: this test serves as a control test for test_flow_sequence """ data = copy.deepcopy(VALID_CFG["flow_sequence-control"]) - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) - def test_flow_sequence(self): + def test_flow_sequence(self, tmp_path): """correctly identify flow sequences""" for i in range(1, 10): data = copy.deepcopy(VALID_CFG[f"flow_sequence-{i}"]) - self._check_via_dict(data, RC_FOUND, dslist=[data.get("ds")]) - - def test_azure_invalid_configuration(self): - """Don't detect incorrect config when invalid datasource_list provided - - If unparsable list is provided we just ignore it. Some users - might assume that since the rest of the configuration is yaml that - multi-line yaml lists are valid (they aren't). When this happens, just - run ds-identify and figure it out for ourselves which platform to run. - """ - self._test_ds_found("Azure-parse-invalid") - - def test_azure_dmi_detection_from_chassis_asset_tag(self): - """Azure datasource is detected from DMI chassis-asset-tag""" - self._test_ds_found("Azure-dmi-detection") - - def test_azure_seed_file_detection(self): - """Azure datasource is detected due to presence of a seed file. - - The seed file tested is /var/lib/cloud/seed/azure/ovf-env.xml.""" - self._test_ds_found("Azure-seed-detection") - - def test_aws_ec2_hvm(self): - """EC2: hvm instances use dmi serial and uuid starting with 'ec2'.""" - self._test_ds_found("Ec2-hvm") - - def test_aws_ec2_hvm_env(self): - """EC2: hvm instances use dmi serial and uuid starting with 'ec2' - - test using SYSTEMD_VIRTUALIZATION, not systemd-detect-virt - """ - self._test_ds_found("Ec2-hvm-env") - - def test_aws_ec2_hvm_endian(self): - """EC2: hvm instances use system-uuid and may have swapped endianness - - test using SYSTEMD_VIRTUALIZATION, not systemd-detect-virt - """ - self._test_ds_found("Ec2-hvm-swap-endianness") - - def test_aws_ec2_xen(self): - """EC2: sys/hypervisor/uuid starts with ec2.""" - self._test_ds_found("Ec2-xen") - - def test_brightbox_is_ec2(self): - """EC2: product_serial ends with '.brightbox.com'""" - self._test_ds_found("Ec2-brightbox") - - def test_bobrightbox_is_not_brightbox(self): - """EC2: bobrightbox.com in product_serial is not brightbox'""" - self._test_ds_not_found("Ec2-brightbox-negative") - - def test_freebsd_nocloud(self): - """NoCloud identified on FreeBSD via label by geom.""" - self._test_ds_found("NoCloud-fbsd") - - def test_gce_by_product_name(self): - """GCE identifies itself with product_name.""" - self._test_ds_found("GCE") - - def test_gce_by_product_name_env(self): - """GCE identifies itself with product_name. - - Uses SYSTEMD_VIRTUALIZATION - """ - self._test_ds_found("GCE_ENV") - - def test_gce_by_serial(self): - """Older gce compute instances must be identified by serial.""" - self._test_ds_found("GCE-serial") - - def test_lxd_kvm(self): - """LXD KVM has race on absent /dev/lxd/socket. Use DMI board_name.""" - self._test_ds_found("LXD-kvm") - - def test_lxd_kvm_jammy(self): - """LXD KVM on host systems with a kernel > 5.10 need to match "qemu". - LXD provides `hv_passthrough` when launching kvm instances when host - kernel is > 5.10. This results in systemd being unable to detect the - virtualized CPUID="Linux KVM Hv" as type "kvm" and results in - systemd-detect-virt returning "qemu" in this case. - - Assert ds-identify can match systemd-detect-virt="qemu" and - /sys/class/dmi/id/board_name = LXD. - Once systemd 251 is available on a target distro, the virtualized - CPUID will be represented properly as "kvm" - """ - self._test_ds_found("LXD-kvm-qemu-kernel-gt-5.10") - - def test_lxd_kvm_jammy_env(self): - """LXD KVM on host systems with a kernel > 5.10 need to match "qemu". - LXD provides `hv_passthrough` when launching kvm instances when host - kernel is > 5.10. This results in systemd being unable to detect the - virtualized CPUID="Linux KVM Hv" as type "kvm" and results in - systemd-detect-virt returning "qemu" in this case. - - Assert ds-identify can match systemd-detect-virt="qemu" and - /sys/class/dmi/id/board_name = LXD. - Once systemd 251 is available on a target distro, the virtualized - CPUID will be represented properly as "kvm" - """ - self._test_ds_found("LXD-kvm-qemu-kernel-gt-5.10-env") - - def test_lxd_containers(self): - """LXD containers will have /dev/lxd/socket at generator time.""" - self._test_ds_found("LXD") - - def test_config_drive(self): - """ConfigDrive datasource has a disk with LABEL=config-2.""" - self._test_ds_found("ConfigDrive") - - def test_rbx_cloud(self): - """Rbx datasource has a disk with LABEL=CLOUDMD.""" - self._test_ds_found("RbxCloud") - - def test_rbx_cloud_lower(self): - """Rbx datasource has a disk with LABEL=cloudmd.""" - self._test_ds_found("RbxCloudLower") - - def test_config_drive_upper(self): - """ConfigDrive datasource has a disk with LABEL=CONFIG-2.""" - self._test_ds_found("ConfigDriveUpper") - - def test_config_drive_seed(self): - """Config Drive seed directory.""" - self._test_ds_found("ConfigDrive-seed") + self._check_via_dict( + data, str(tmp_path), RC_FOUND, dslist=[data.get("ds")] + ) - def test_config_drive_interacts_with_ibmcloud_config_disk(self): + def test_config_drive_interacts_with_ibmcloud_config_disk(self, tmp_path): """Verify ConfigDrive interaction with IBMCloud. If ConfigDrive is enabled and not IBMCloud, then ConfigDrive @@ -697,24 +932,15 @@ def test_config_drive_interacts_with_ibmcloud_config_disk(self): # with list including IBMCloud, config drive should be not found. files[cfgpath] = "datasource_list: [ ConfigDrive, IBMCloud ]\n" - ret = self._check_via_dict(data, shell_true) - self.assertEqual(ret.cfg.get("datasource_list"), ["IBMCloud", "None"]) + ret = self._check_via_dict(data, str(tmp_path / "ibm"), shell_true) + assert ret.cfg.get("datasource_list") == ["IBMCloud", "None"] # But if IBMCloud is not enabled, config drive should claim this. files[cfgpath] = "datasource_list: [ ConfigDrive, NoCloud ]\n" - ret = self._check_via_dict(data, shell_true) - self.assertEqual( - ret.cfg.get("datasource_list"), ["ConfigDrive", "None"] - ) - - @pytest.mark.xfail( - reason=("not supported: yaml parser implemented in POSIX shell") - ) - def test_multiline_yaml(self): - """Multi-line yaml is unsupported""" - self._test_ds_found("LXD-kvm-not-azure") + ret = self._check_via_dict(data, str(tmp_path), shell_true) + assert ret.cfg.get("datasource_list") == ["ConfigDrive", "None"] - def test_ibmcloud_template_userdata_in_provisioning(self): + def test_ibmcloud_template_userdata_in_provisioning(self, tmp_path): """Template provisioned with user-data during provisioning stage. Template provisioning with user-data has METADATA disk, @@ -725,16 +951,9 @@ def test_ibmcloud_template_userdata_in_provisioning(self): m for m in data["mocks"] if m["name"] == "is_ibm_provisioning" ][0] isprov_m["ret"] = shell_true - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) - def test_ibmcloud_template_userdata(self): - """Template provisioned with user-data first boot. - - Template provisioning with user-data has METADATA disk. - datasource should return found.""" - self._test_ds_found("IBMCloud-metadata") - - def test_ibmcloud_template_no_userdata_in_provisioning(self): + def test_ibmcloud_template_no_userdata_in_provisioning(self, tmp_path): """Template provisioned with no user-data during provisioning. no disks attached. Datasource should return not found.""" @@ -742,19 +961,17 @@ def test_ibmcloud_template_no_userdata_in_provisioning(self): data["mocks"].append( {"name": "is_ibm_provisioning", "ret": shell_true} ) - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) - def test_ibmcloud_template_no_userdata(self): + def test_ibmcloud_template_no_userdata(self, tmp_path): """Template provisioned with no user-data first boot. no disks attached. Datasource should return found.""" - self._check_via_dict(VALID_CFG["IBMCloud-nodisks"], RC_NOT_FOUND) - - def test_ibmcloud_os_code(self): - """Launched by os code always has config-2 disk.""" - self._test_ds_found("IBMCloud-config-2") + self._check_via_dict( + VALID_CFG["IBMCloud-nodisks"], str(tmp_path), RC_NOT_FOUND + ) - def test_ibmcloud_os_code_different_uuid(self): + def test_ibmcloud_os_code_different_uuid(self, tmp_path): """IBM cloud config-2 disks must be explicit match on UUID. If the UUID is not 9796-932E then we actually expect ConfigDrive.""" @@ -770,10 +987,10 @@ def test_ibmcloud_os_code_different_uuid(self): ds_ibm.IBM_CONFIG_UUID, "DEAD-BEEF" ) self._check_via_dict( - data, rc=RC_FOUND, dslist=["ConfigDrive", DS_NONE] + data, str(tmp_path), rc=RC_FOUND, dslist=["ConfigDrive", DS_NONE] ) - def test_ibmcloud_with_nocloud_seed(self): + def test_ibmcloud_with_nocloud_seed(self, tmp_path): """NoCloud seed should be preferred over IBMCloud. A nocloud seed should be preferred over IBMCloud even if enabled. @@ -783,12 +1000,12 @@ def test_ibmcloud_with_nocloud_seed(self): if not files: data["files"] = files files.update(VALID_CFG["NoCloud-seed"]["files"]) - ret = self._check_via_dict(data, shell_true) - self.assertEqual( - ["NoCloud", "IBMCloud", "None"], ret.cfg.get("datasource_list") + ret = self._check_via_dict(data, str(tmp_path), shell_true) + assert ["NoCloud", "IBMCloud", "None"] == ret.cfg.get( + "datasource_list" ) - def test_ibmcloud_with_configdrive_seed(self): + def test_ibmcloud_with_configdrive_seed(self, tmp_path): """ConfigDrive seed should be preferred over IBMCloud. A ConfigDrive seed should be preferred over IBMCloud even if enabled. @@ -799,26 +1016,28 @@ def test_ibmcloud_with_configdrive_seed(self): if not files: data["files"] = files files.update(VALID_CFG["ConfigDrive-seed"]["files"]) - ret = self._check_via_dict(data, shell_true) - self.assertEqual( - ["ConfigDrive", "IBMCloud", "None"], ret.cfg.get("datasource_list") + ret = self._check_via_dict(data, str(tmp_path), shell_true) + assert ["ConfigDrive", "IBMCloud", "None"] == ret.cfg.get( + "datasource_list" ) - def test_policy_disabled(self): + def test_policy_disabled(self, tmp_path): """A Builtin policy of 'disabled' should return not found. Even though a search would find something, the builtin policy of disabled should cause the return of not found.""" mydata = copy.deepcopy(VALID_CFG["Ec2-hvm"]) - self._check_via_dict(mydata, rc=RC_NOT_FOUND, policy_dmi="disabled") + self._check_via_dict( + mydata, str(tmp_path), rc=RC_NOT_FOUND, policy_dmi="disabled" + ) - def test_policy_config_disable_overrides_builtin(self): + def test_policy_config_disable_overrides_builtin(self, tmp_path): """explicit policy: disabled in config file should cause not found.""" mydata = copy.deepcopy(VALID_CFG["Ec2-hvm"]) mydata["files"][P_DSID_CFG] = "\n".join(["policy: disabled", ""]) - self._check_via_dict(mydata, rc=RC_NOT_FOUND) + self._check_via_dict(mydata, str(tmp_path), rc=RC_NOT_FOUND) - def test_single_entry_defines_datasource(self): + def test_single_entry_defines_datasource(self, tmp_path): """If config has a single entry in datasource_list, that is used. Test the valid Ec2-hvm, but provide a config file that specifies @@ -827,9 +1046,11 @@ def test_single_entry_defines_datasource(self): mydata = copy.deepcopy(VALID_CFG["Ec2-hvm"]) cfgpath = "etc/cloud/cloud.cfg.d/myds.cfg" mydata["files"][cfgpath] = 'datasource_list: ["NoCloud"]\n' - self._check_via_dict(mydata, rc=RC_FOUND, dslist=["NoCloud"]) + self._check_via_dict( + mydata, str(tmp_path), rc=RC_FOUND, dslist=["NoCloud"] + ) - def test_configured_list_with_none(self): + def test_configured_list_with_none(self, tmp_path): """When datasource_list already contains None, None is not added. The explicitly configured datasource_list has 'None' in it. That @@ -837,9 +1058,11 @@ def test_configured_list_with_none(self): mydata = copy.deepcopy(VALID_CFG["GCE"]) cfgpath = "etc/cloud/cloud.cfg.d/myds.cfg" mydata["files"][cfgpath] = 'datasource_list: ["Ec2", "None"]\n' - self._check_via_dict(mydata, rc=RC_FOUND, dslist=["Ec2", DS_NONE]) + self._check_via_dict( + mydata, str(tmp_path), rc=RC_FOUND, dslist=["Ec2", DS_NONE] + ) - def test_nocloud_seedfrom(self): + def test_nocloud_seedfrom(self, tmp_path): """Check seedfrom system config detects nocloud. Verify that a cloud.cfg.d/ that contains more than two datasources in @@ -848,11 +1071,12 @@ def test_nocloud_seedfrom(self): """ self._check_via_dict( copy.deepcopy(VALID_CFG["NoCloud-seedfrom"]), + str(tmp_path), rc=RC_FOUND, dslist=["NoCloud", DS_NONE], ) - def test_nocloud_userdata_and_metadata(self): + def test_nocloud_userdata_and_metadata(self, tmp_path): """Check seedfrom system config detects nocloud. Verify that a cloud.cfg.d/ that contains more than two datasources in @@ -861,58 +1085,27 @@ def test_nocloud_userdata_and_metadata(self): """ self._check_via_dict( copy.deepcopy(VALID_CFG["NoCloud-user-data-meta-data"]), + str(tmp_path), rc=RC_FOUND, dslist=["NoCloud", DS_NONE], ) - def test_aliyun_identified(self): - """Test that Aliyun cloud is identified by product id.""" - self._test_ds_found("AliYun") - - def test_aliyun_over_ec2(self): + def test_aliyun_over_ec2(self, tmp_path): """Even if all other factors identified Ec2, AliYun should be used.""" mydata = copy.deepcopy(VALID_CFG["Ec2-xen"]) - self._test_ds_found("AliYun") + self._test_ds_found("AliYun", str(tmp_path)) prod_name = VALID_CFG["AliYun"]["files"][P_PRODUCT_NAME] mydata["files"][P_PRODUCT_NAME] = prod_name policy = "search,found=first,maybe=none,notfound=disabled" self._check_via_dict( - mydata, rc=RC_FOUND, dslist=["AliYun", DS_NONE], policy_dmi=policy + mydata, + str(tmp_path), + rc=RC_FOUND, + dslist=["AliYun", DS_NONE], + policy_dmi=policy, ) - def test_default_openstack_intel_is_found(self): - """On Intel, openstack must be identified.""" - self._test_ds_found("OpenStack") - - def test_openstack_open_telekom_cloud(self): - """Open Telecom identification.""" - self._test_ds_found("OpenStack-OpenTelekom") - - def test_openstack_sap_ccloud(self): - """SAP Converged Cloud identification""" - self._test_ds_found("OpenStack-SAPCCloud") - - def test_openstack_sap_ccloud_env(self): - """SAP Converged Cloud identification""" - self._test_ds_found("OpenStack-SAPCCloud-env") - - def test_openstack_huawei_cloud(self): - """Open Huawei Cloud identification.""" - self._test_ds_found("OpenStack-HuaweiCloud") - - def test_openstack_samsung_cloud_platform(self): - """Open Samsung Cloud Platform identification.""" - self._test_ds_found("OpenStack-SamsungCloudPlatform") - - def test_openstack_asset_tag_nova(self): - """OpenStack identification via asset tag OpenStack Nova.""" - self._test_ds_found("OpenStack-AssetTag-Nova") - - def test_openstack_asset_tag_copute(self): - """OpenStack identification via asset tag OpenStack Compute.""" - self._test_ds_found("OpenStack-AssetTag-Compute") - - def test_openstack_on_non_intel_is_maybe(self): + def test_openstack_on_non_intel_is_maybe(self, tmp_path): """On non-Intel, openstack without dmi info is none. nova does not identify itself on platforms other than intel. @@ -929,50 +1122,52 @@ def test_openstack_on_non_intel_is_maybe(self): # this should show not found as default uname in tests is intel. # and intel openstack requires positive identification. - self._check_via_dict(data, RC_NOT_FOUND, dslist=None) + self._check_via_dict( + data, str(tmp_path / "0"), RC_NOT_FOUND, dslist=None + ) # updating the uname to ppc64 though should get a maybe. data.update({"mocks": [MOCK_VIRT_IS_KVM, MOCK_UNAME_IS_PPC64]}) - (_, _, err, _, _) = self._check_via_dict(data, RC_NOT_FOUND) - self.assertIn("check for 'OpenStack' returned maybe", err) - self.assertIn("No ds found", err) - self.assertIn("Disabled cloud-init", err) - self.assertIn("returning 1", err) - - def test_default_ovf_is_found(self): - """OVF is identified found when ovf/ovf-env.xml seed file exists.""" - self._test_ds_found("OVF-seed") + (_, _, err, _, _) = self._check_via_dict( + data, str(tmp_path / "1"), RC_NOT_FOUND + ) + assert "check for 'OpenStack' returned maybe" in err + assert "No ds found" in err + assert "Disabled cloud-init" in err + assert "returning 1" in err - def test_default_ovf_with_detect_virt_none_not_found(self): + def test_default_ovf_with_detect_virt_none_not_found(self, tmp_path): """OVF identifies not found when detect_virt returns "none".""" self._check_via_dict( - {"ds": "OVF"}, rc=RC_NOT_FOUND, policy_dmi="disabled" + {"ds": "OVF"}, + str(tmp_path), + rc=RC_NOT_FOUND, + policy_dmi="disabled", ) - def test_default_ovf_returns_not_found_on_azure(self): + def test_default_ovf_returns_not_found_on_azure(self, tmp_path): """OVF datasource won't be found as false positive on Azure.""" ovfonazure = copy.deepcopy(VALID_CFG["OVF"]) # Set azure asset tag to assert OVF content not found ovfonazure["files"][ P_CHASSIS_ASSET_TAG ] = "7783-7084-3265-9085-8269-3286-77\n" - self._check_via_dict(ovfonazure, RC_FOUND, dslist=["Azure", DS_NONE]) - - def test_ovf_on_vmware_iso_found_by_cdrom_with_ovf_schema_match(self): - """OVF is identified when iso9660 cdrom path contains ovf schema.""" - self._test_ds_found("OVF") - - def test_ovf_on_vmware_guestinfo_found(self): - """OVF guest info is found on vmware.""" - self._test_ds_found("OVF-guestinfo") + self._check_via_dict( + ovfonazure, str(tmp_path), RC_FOUND, dslist=["Azure", DS_NONE] + ) - def test_ovf_on_vmware_iso_found_by_cdrom_with_matching_fs_label(self): + def test_ovf_on_vmware_iso_found_by_cdrom_with_matching_fs_label( + self, tmp_path + ): """OVF is identified by well-known iso9660 labels.""" ovf_cdrom_by_label = copy.deepcopy(VALID_CFG["OVF"]) # Unset matching cdrom ovf schema content ovf_cdrom_by_label["files"]["dev/sr0"] = "No content match" self._check_via_dict( - ovf_cdrom_by_label, rc=RC_NOT_FOUND, policy_dmi="disabled" + ovf_cdrom_by_label, + str(tmp_path), + rc=RC_NOT_FOUND, + policy_dmi="disabled", ) # Add recognized labels @@ -997,103 +1192,76 @@ def test_ovf_on_vmware_iso_found_by_cdrom_with_matching_fs_label(self): ] ) self._check_via_dict( - ovf_cdrom_by_label, rc=RC_FOUND, dslist=["OVF", DS_NONE] + ovf_cdrom_by_label, + str(tmp_path), + rc=RC_FOUND, + dslist=["OVF", DS_NONE], ) - def test_ovf_on_vmware_iso_found_by_cdrom_with_different_size(self): + def test_ovf_on_vmware_iso_found_by_cdrom_with_different_size( + self, tmp_path + ): """OVF is identified by well-known iso9660 labels.""" ovf_cdrom_with_size = copy.deepcopy(VALID_CFG["OVF"]) # Set cdrom size to 20480 (10MB in 512 byte units) ovf_cdrom_with_size["files"]["sys/class/block/sr0/size"] = "20480\n" self._check_via_dict( - ovf_cdrom_with_size, rc=RC_NOT_FOUND, policy_dmi="disabled" + ovf_cdrom_with_size, + str(tmp_path), + rc=RC_NOT_FOUND, + policy_dmi="disabled", ) # Set cdrom size to 204800 (100MB in 512 byte units) ovf_cdrom_with_size["files"]["sys/class/block/sr0/size"] = "204800\n" self._check_via_dict( - ovf_cdrom_with_size, rc=RC_NOT_FOUND, policy_dmi="disabled" + ovf_cdrom_with_size, + str(tmp_path), + rc=RC_NOT_FOUND, + policy_dmi="disabled", ) # Set cdrom size to 18432 (9MB in 512 byte units) ovf_cdrom_with_size["files"]["sys/class/block/sr0/size"] = "18432\n" self._check_via_dict( - ovf_cdrom_with_size, rc=RC_FOUND, dslist=["OVF", DS_NONE] + ovf_cdrom_with_size, + str(tmp_path), + rc=RC_FOUND, + dslist=["OVF", DS_NONE], ) # Set cdrom size to 2048 (1MB in 512 byte units) ovf_cdrom_with_size["files"]["sys/class/block/sr0/size"] = "2048\n" self._check_via_dict( - ovf_cdrom_with_size, rc=RC_FOUND, dslist=["OVF", DS_NONE] + ovf_cdrom_with_size, + str(tmp_path), + rc=RC_FOUND, + dslist=["OVF", DS_NONE], ) - def test_default_nocloud_as_vdb_iso9660(self): - """NoCloud is found with iso9660 filesystem on non-cdrom disk.""" - self._test_ds_found("NoCloud") - - def test_nocloud_upper(self): - """NoCloud is found with uppercase filesystem label.""" - self._test_ds_found("NoCloudUpper") - - def test_nocloud_seed_in_cfg(self): - """NoCloud seed definition can go in /etc/cloud/cloud.cfg[.d]""" - self._test_ds_found("NoCloud-cfg") - - def test_nocloud_fatboot(self): - """NoCloud fatboot label - LP: #184166.""" - self._test_ds_found("NoCloud-fatboot") - - def test_nocloud_seed(self): - """Nocloud seed directory.""" - self._test_ds_found("NoCloud-seed") - - def test_nocloud_seed_ubuntu_core_writable(self): - """Nocloud seed directory ubuntu core writable""" - self._test_ds_found("NoCloud-seed-ubuntu-core") - - def test_hetzner_found(self): - """Hetzner cloud is identified in sys_vendor.""" - self._test_ds_found("Hetzner") - - def test_cloudcix_found(self): - """CloudCIX cloud is identified in dmi product-name""" - self._test_ds_found("CloudCIX") - - def test_nwcs_found(self): - """NWCS is identified in sys_vendor.""" - self._test_ds_found("NWCS") - - def test_smartos_bhyve(self): - """SmartOS cloud identified by SmartDC in dmi.""" - self._test_ds_found("SmartOS-bhyve") - - def test_smartos_lxbrand(self): - """SmartOS cloud identified on lxbrand container.""" - self._test_ds_found("SmartOS-lxbrand") - - def test_smartos_lxbrand_env(self): - """SmartOS cloud identified on lxbrand container.""" - self._test_ds_found("SmartOS-lxbrand-env") - - def test_smartos_lxbrand_requires_socket(self): + def test_smartos_lxbrand_requires_socket(self, tmp_path): """SmartOS cloud should not be identified if no socket file.""" mycfg = copy.deepcopy(VALID_CFG["SmartOS-lxbrand"]) del mycfg["files"][ds_smartos.METADATA_SOCKFILE] - self._check_via_dict(mycfg, rc=RC_NOT_FOUND, policy_dmi="disabled") + self._check_via_dict( + mycfg, str(tmp_path), rc=RC_NOT_FOUND, policy_dmi="disabled" + ) - def test_smartos_lxbrand_requires_socket_env(self): + def test_smartos_lxbrand_requires_socket_env(self, tmp_path): """SmartOS cloud should not be identified if no socket file.""" mycfg = copy.deepcopy(VALID_CFG["SmartOS-lxbrand-env"]) del mycfg["files"][ds_smartos.METADATA_SOCKFILE] - self._check_via_dict(mycfg, rc=RC_NOT_FOUND, policy_dmi="disabled") + self._check_via_dict( + mycfg, str(tmp_path), rc=RC_NOT_FOUND, policy_dmi="disabled" + ) - def test_path_env_gets_set_from_main(self): + def test_path_env_gets_set_from_main(self, tmp_path): """PATH environment should always have some tokens when main is run. We explicitly call main as we want to ensure it updates PATH.""" cust = copy.deepcopy(VALID_CFG["NoCloud"]) - rootd = self.tmp_dir() + rootd = str(tmp_path) mpp = "main-printpath" pre = "MYPATH=" cust["files"][mpp] = ( @@ -1101,75 +1269,21 @@ def test_path_env_gets_set_from_main(self): ) ret = self._check_via_dict( cust, + rootd, RC_FOUND, func=".", args=[os.path.join(rootd, mpp)], - rootd=rootd, ) match = [ line for line in ret.stdout.splitlines() if line.startswith(pre) ][0] toks = match.replace(pre, "").split(":") expected = ["/sbin", "/bin", "/usr/sbin", "/usr/bin", "/mycust/path"] - self.assertEqual( - expected, - [p for p in expected if p in toks], - "path did not have expected tokens", - ) - - def test_zstack_is_ec2(self): - """EC2: chassis asset tag ends with 'zstack.io'""" - self._test_ds_found("Ec2-ZStack") - - def test_e24cloud_is_ec2(self): - """EC2: e24cloud identified by sys_vendor""" - self._test_ds_found("Ec2-E24Cloud") - - def test_e24cloud_not_active(self): - """EC2: bobrightbox.com in product_serial is not brightbox'""" - self._test_ds_not_found("Ec2-E24Cloud-negative") - - def test_outscale_is_ec2(self): - """EC2: outscale identified by sys_vendor and product_name""" - self._test_ds_found("Ec2-Outscale") - - def test_outscale_not_active_sysvendor(self): - """EC2: outscale in sys_vendor is not outscale'""" - self._test_ds_not_found("Ec2-Outscale-negative-sysvendor") + assert expected == [ + p for p in expected if p in toks + ], "path did not have expected tokens" - def test_outscale_not_active_productname(self): - """EC2: outscale in product_name is not outscale'""" - self._test_ds_not_found("Ec2-Outscale-negative-productname") - - def test_vmware_no_valid_transports(self): - """VMware: no valid transports""" - self._test_ds_not_found("VMware-NoValidTransports") - - def test_vmware_on_vmware_when_vmware_customization_is_enabled(self): - """VMware is identified when vmware customization is enabled.""" - self._test_ds_found("VMware-vmware-customization") - - def test_vmware_ovf_on_vmware_with_vmware_customization_and_ovf_schema( - self, - ): - """VMware and OVF are identified when: - 1. On VMware platform. - 2. VMware customization is enabled. - 3. iso9660 cdrom path contains ovf schema.""" - self._test_ds_found( - "VMware-OVF-on-vmware-with-vmware-customization-and-ovf-schema" - ) - - def test_ovf_not_on_vmware_with_vmware_customization_and_ovf_schema(self): - """OVF is identified when: - 1. Not on VMware platform. - 2. VMware customization is enabled. - 3. iso9660 cdrom path contains ovf schema.""" - self._test_ds_found( - "OVF-not-on-vmware-with-vmware-customization-and-ovf-schema" - ) - - def test_vmware_on_vmware_open_vm_tools_64(self): + def test_vmware_on_vmware_open_vm_tools_64(self, tmp_path): """VMware is identified when open-vm-tools installed in /usr/lib64.""" cust64 = copy.deepcopy(VALID_CFG["VMware-vmware-customization"]) p32 = "usr/lib/vmware-tools/plugins/vmsvc/libdeployPkgPlugin.so" @@ -1177,10 +1291,10 @@ def test_vmware_on_vmware_open_vm_tools_64(self): cust64["files"][open64] = cust64["files"][p32] del cust64["files"][p32] self._check_via_dict( - cust64, RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] + cust64, str(tmp_path), RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] ) - def test_vmware_on_vmware_open_vm_tools_x86_64_linux_gnu(self): + def test_vmware_on_vmware_open_vm_tools_x86_64_linux_gnu(self, tmp_path): """VMware is identified when open-vm-tools installed in /usr/lib/x86_64-linux-gnu.""" cust64 = copy.deepcopy(VALID_CFG["VMware-vmware-customization"]) @@ -1192,10 +1306,10 @@ def test_vmware_on_vmware_open_vm_tools_x86_64_linux_gnu(self): cust64["files"][x86] = cust64["files"][p32] del cust64["files"][p32] self._check_via_dict( - cust64, RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] + cust64, str(tmp_path), RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] ) - def test_vmware_on_vmware_open_vm_tools_aarch64_linux_gnu(self): + def test_vmware_on_vmware_open_vm_tools_aarch64_linux_gnu(self, tmp_path): """VMware is identified when open-vm-tools installed in /usr/lib/aarch64-linux-gnu.""" cust64 = copy.deepcopy(VALID_CFG["VMware-vmware-customization"]) @@ -1207,10 +1321,10 @@ def test_vmware_on_vmware_open_vm_tools_aarch64_linux_gnu(self): cust64["files"][aarch64] = cust64["files"][p32] del cust64["files"][p32] self._check_via_dict( - cust64, RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] + cust64, str(tmp_path), RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] ) - def test_vmware_on_vmware_open_vm_tools_i386_linux_gnu(self): + def test_vmware_on_vmware_open_vm_tools_i386_linux_gnu(self, tmp_path): """VMware is identified when open-vm-tools installed in /usr/lib/i386-linux-gnu.""" cust64 = copy.deepcopy(VALID_CFG["VMware-vmware-customization"]) @@ -1222,121 +1336,29 @@ def test_vmware_on_vmware_open_vm_tools_i386_linux_gnu(self): cust64["files"][i386] = cust64["files"][p32] del cust64["files"][p32] self._check_via_dict( - cust64, RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] - ) - - def test_vmware_envvar_no_data(self): - """VMware: envvar transport no data""" - self._test_ds_not_found("VMware-EnvVar-NoData") - - def test_vmware_envvar_no_virt_id(self): - """VMware: envvar transport success if no virt id""" - self._test_ds_found("VMware-EnvVar-NoVirtID") - - def test_vmware_envvar_activated_by_metadata(self): - """VMware: envvar transport activated by metadata""" - self._test_ds_found("VMware-EnvVar-Metadata") - - def test_vmware_envvar_activated_by_userdata(self): - """VMware: envvar transport activated by userdata""" - self._test_ds_found("VMware-EnvVar-Userdata") - - def test_vmware_envvar_activated_by_vendordata(self): - """VMware: envvar transport activated by vendordata""" - self._test_ds_found("VMware-EnvVar-Vendordata") - - def test_vmware_guestinfo_no_data(self): - """VMware: guestinfo transport no data""" - self._test_ds_not_found("VMware-GuestInfo-NoData-Rpctool") - self._test_ds_not_found("VMware-GuestInfo-NoData-Vmtoolsd") - - def test_vmware_guestinfo_no_virt_id(self): - """VMware: guestinfo transport fails if no virt id""" - self._test_ds_not_found("VMware-GuestInfo-NoVirtID") - - def test_vmware_guestinfo_activated_by_metadata(self): - """VMware: guestinfo transport activated by metadata""" - self._test_ds_found("VMware-GuestInfo-Metadata") - - def test_vmware_guestinfo_activated_by_userdata(self): - """VMware: guestinfo transport activated by userdata""" - self._test_ds_found("VMware-GuestInfo-Userdata") - - def test_vmware_guestinfo_activated_by_vendordata(self): - """VMware: guestinfo transport activated by vendordata""" - self._test_ds_found("VMware-GuestInfo-Vendordata") - - def test_vmware_ovf_on_vmware_with_guestinfo_metadata_and_ovf_schema(self): - """VMware and OVF are identified when: - 1. On VMware platform. - 2. guestinfo transport activated by metadata - 3. iso9660 cdrom path contains ovf schema.""" - self._test_ds_found( - "VMware-OVF-on-vmware-with-guestinfo-metadata-and-ovf-schema" - ) - - def test_ovf_not_on_vmware_with_guestinfo_metadata_and_ovf_schema(self): - """OVF is identified when: - 1. Not on VMware platform. - 2. guestinfo transport activated by metadata - 3. iso9660 cdrom path contains ovf schema.""" - self._test_ds_found( - "OVF-not-on-vmware-with-guestinfo-metadata-and-ovf-schema" + cust64, str(tmp_path), RC_FOUND, dslist=[cust64.get("ds"), DS_NONE] ) +@pytest.mark.allow_subp_for("sh") class TestAkamai(DsIdentifyBase): - def test_found_by_sys_vendor(self): - """ds-identify finds Akamai by system-manufacturer dmi field""" - self._test_ds_found("Akamai") - - def test_found_by_sys_vendor_akamai(self): + def test_found_by_sys_vendor_akamai(self, tmp_path): """ ds-identify finds Akamai by system-manufacturer dmi field when set with name "Akamai" (expected in the future) """ cfg = copy.deepcopy(VALID_CFG["Akamai"]) cfg["mocks"][0]["RET"] = "Akamai" - self._check_via_dict(cfg, rc=RC_FOUND) + self._check_via_dict(cfg, str(tmp_path), rc=RC_FOUND) - def test_not_found(self): + def test_not_found(self, tmp_path): """ds-identify does not find Akamai by system-manufacturer field""" cfg = copy.deepcopy(VALID_CFG["Akamai"]) cfg["mocks"][0]["RET"] = "Other" - self._check_via_dict(cfg, rc=RC_NOT_FOUND) - - -class TestBSDNoSys(DsIdentifyBase): - """Test *BSD code paths - - FreeBSD doesn't have /sys so we use kenv(1) here. - OpenBSD uses sysctl(8). - Other BSD systems fallback to dmidecode(8). - BSDs also doesn't have systemd-detect-virt(8), so we use sysctl(8) to query - kern.vm_guest, and optionally map it""" - - def test_dmi_kenv(self): - """Test that kenv(1) works on systems which don't have /sys - - This will be used on FreeBSD systems. - """ - self._test_ds_found("Hetzner-kenv") - - def test_dmi_sysctl(self): - """Test that sysctl(8) works on systems which don't have /sys - - This will be used on OpenBSD systems. - """ - self._test_ds_found("Hetzner-sysctl") - - def test_dmi_dmidecode(self): - """Test that dmidecode(8) works on systems which don't have /sys - - This will be used on all other BSD systems. - """ - self._test_ds_found("Hetzner-dmidecode") + self._check_via_dict(cfg, str(tmp_path), rc=RC_NOT_FOUND) +@pytest.mark.allow_subp_for("sh") class TestIsIBMProvisioning(DsIdentifyBase): """Test the is_ibm_provisioning method in ds-identify.""" @@ -1345,19 +1367,23 @@ class TestIsIBMProvisioning(DsIdentifyBase): boot_ref = "/proc/1/environ" funcname = "is_ibm_provisioning" - def test_no_config(self): + def test_no_config(self, tmp_path): """No provisioning config means not provisioning.""" - ret = self.call(files={}, func=self.funcname) - self.assertEqual(shell_false, ret.rc) + ret = self.call(str(tmp_path), files={}, func=self.funcname) + assert shell_false == ret.rc - def test_config_only(self): + def test_config_only(self, tmp_path): """A provisioning config without a log means provisioning.""" - ret = self.call(files={self.prov_cfg: "key=value"}, func=self.funcname) - self.assertEqual(shell_true, ret.rc) + ret = self.call( + str(tmp_path), + files={self.prov_cfg: "key=value"}, + func=self.funcname, + ) + assert shell_true == ret.rc - def test_config_with_old_log(self): + def test_config_with_old_log(self, tmp_path): """A config with a log from previous boot is not provisioning.""" - rootd = self.tmp_dir() + rootd = str(tmp_path) data = { self.prov_cfg: ("key=value\nkey2=val2\n", -10), self.inst_log: ("log data\n", -30), @@ -1365,12 +1391,12 @@ def test_config_with_old_log(self): } populate_dir_with_ts(rootd, data) ret = self.call(rootd=rootd, func=self.funcname) - self.assertEqual(shell_false, ret.rc) - self.assertIn("from previous boot", ret.stderr) + assert shell_false == ret.rc + assert "from previous boot" in ret.stderr - def test_config_with_new_log(self): + def test_config_with_new_log(self, tmp_path): """A config with a log from this boot is provisioning.""" - rootd = self.tmp_dir() + rootd = str(tmp_path) data = { self.prov_cfg: ("key=value\nkey2=val2\n", -10), self.inst_log: ("log data\n", 30), @@ -1378,32 +1404,22 @@ def test_config_with_new_log(self): } populate_dir_with_ts(rootd, data) ret = self.call(rootd=rootd, func=self.funcname) - self.assertEqual(shell_true, ret.rc) - self.assertIn("from current boot", ret.stderr) + assert shell_true == ret.rc + assert "from current boot" in ret.stderr class TestOracle(DsIdentifyBase): - def test_found_by_chassis(self): - """Simple positive test of Oracle by chassis id.""" - self._test_ds_found("Oracle") - - def test_not_found(self): + @pytest.mark.allow_subp_for("sh") + def test_not_found(self, tmp_path): """Simple negative test of Oracle.""" mycfg = copy.deepcopy(VALID_CFG["Oracle"]) mycfg["files"][P_CHASSIS_ASSET_TAG] = "Not Oracle" - self._check_via_dict(mycfg, rc=RC_NOT_FOUND) + self._check_via_dict(mycfg, str(tmp_path), rc=RC_NOT_FOUND) +@pytest.mark.allow_subp_for("sh") class TestWSL(DsIdentifyBase): - def test_not_found_virt(self): - """Simple negative test for WSL due other virt.""" - self._test_ds_not_found("Not-WSL") - - def test_no_fs_mounts(self): - """Negative test by lack of host filesystem mount points.""" - self._test_ds_not_found("WSL-no-host-mounts") - - def test_no_userprofile(self): + def test_no_userprofile(self, tmp_path): """Negative test by failing to read the %USERPROFILE% environment variable. """ @@ -1415,12 +1431,12 @@ def test_no_userprofile(self): "RET": "\r\n", }, ) - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) - def test_no_cloudinitdir_in_userprofile(self): + def test_no_cloudinitdir_in_userprofile(self, tmp_path): """Negative test by not finding %USERPROFILE%/.cloud-init.""" data = copy.deepcopy(VALID_CFG["WSL-supported"]) - userprofile = self.tmp_dir() + userprofile = str(tmp_path) data["mocks"].append( { "name": "WSL_profile_dir", @@ -1428,12 +1444,12 @@ def test_no_cloudinitdir_in_userprofile(self): "RET": userprofile, }, ) - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) - def test_empty_cloudinitdir(self): + def test_empty_cloudinitdir(self, tmp_path): """Negative test by lack of host filesystem mount points.""" data = copy.deepcopy(VALID_CFG["WSL-supported"]) - userprofile = self.tmp_dir() + userprofile = str(tmp_path) data["mocks"].append( { "name": "WSL_profile_dir", @@ -1443,14 +1459,14 @@ def test_empty_cloudinitdir(self): ) cloudinitdir = os.path.join(userprofile, ".cloud-init") os.mkdir(cloudinitdir) - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) - def test_found_fail_due_instance_name_parsing(self): + def test_found_fail_due_instance_name_parsing(self, tmp_path): """WSL datasource detection fail due parsing error even though the file exists. """ data = copy.deepcopy(VALID_CFG["WSL-supported-debian"]) - userprofile = self.tmp_dir() + userprofile = str(tmp_path) data["mocks"].append( { "name": "WSL_profile_dir", @@ -1469,13 +1485,13 @@ def test_found_fail_due_instance_name_parsing(self): os.mkdir(cloudinitdir) filename = os.path.join(cloudinitdir, "cant-findme.user-data") Path(filename).touch() - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path), RC_NOT_FOUND) Path(filename).unlink() - def test_found_via_userdata_version_codename(self): + def test_found_via_userdata_version_codename(self, tmp_path): """WSL datasource detected by VERSION_CODENAME when no VERSION_ID""" data = copy.deepcopy(VALID_CFG["WSL-supported-debian"]) - userprofile = self.tmp_dir() + userprofile = str(tmp_path) data["mocks"].append( { "name": "WSL_profile_dir", @@ -1487,15 +1503,17 @@ def test_found_via_userdata_version_codename(self): os.mkdir(cloudinitdir) filename = os.path.join(cloudinitdir, "debian-trixie.user-data") Path(filename).touch() - self._check_via_dict(data, RC_FOUND, dslist=[data.get("ds"), DS_NONE]) + self._check_via_dict( + data, str(tmp_path), RC_FOUND, dslist=[data.get("ds"), DS_NONE] + ) Path(filename).unlink() - def test_found_via_userdata(self): + def test_found_via_userdata(self, tmp_path): """ WSL datasource is found on applicable userdata files in cloudinitdir. """ data = copy.deepcopy(VALID_CFG["WSL-supported"]) - userprofile = self.tmp_dir() + userprofile = str(tmp_path) data["mocks"].append( { "name": "WSL_profile_dir", @@ -1530,16 +1548,19 @@ def test_found_via_userdata(self): os.path.join(cloudinitdir, "default.user-data"), ] - for filename in userdata_files: + for i, filename in enumerate(userdata_files): Path(filename).touch() self._check_via_dict( - data, RC_FOUND, dslist=[data.get("ds"), DS_NONE] + data, + str(tmp_path / str(i)), + RC_FOUND, + dslist=[data.get("ds"), DS_NONE], ) # Delete one by one Path(filename).unlink() # Until there is none, making the datasource no longer viable. - self._check_via_dict(data, RC_NOT_FOUND) + self._check_via_dict(data, str(tmp_path / "-final"), RC_NOT_FOUND) def blkid_out(disks=None): From 21961048a694a8a16bae4c8cbd8c477b1b9687d9 Mon Sep 17 00:00:00 2001 From: ibrahim-mojalled <156794295+ibrahim-mojalled@users.noreply.github.com> Date: Mon, 30 Jun 2025 23:55:30 +0300 Subject: [PATCH 007/150] docs(mounts): Correct fs-freq reference to fs_freq (#6262) Corrects the spelling of the fstab 'fs_freq' field from 'fs-freq' to 'fs_freq' in documentation, examples, and comments. Fixes GH-6210 --- cloudinit/config/cc_mounts.py | 2 +- cloudinit/config/schemas/schema-cloud-config-v1.json | 2 +- doc/examples/cloud-config-mount-points.txt | 2 +- doc/examples/cloud-config.txt | 2 +- doc/module-docs/cc_mounts/data.yaml | 2 +- doc/rtd/reference/yaml_examples/mounts.rst | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py index bf9d80327d3..a4bc44abdad 100644 --- a/cloudinit/config/cc_mounts.py +++ b/cloudinit/config/cc_mounts.py @@ -516,7 +516,7 @@ def mount_if_needed( def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: """Handle the mounts configuration.""" - # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno + # fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno uses_systemd = cloud.distro.uses_systemd() default_mount_options = ( "defaults,nofail,x-systemd.after=cloud-init-network.service,_netdev" diff --git a/cloudinit/config/schemas/schema-cloud-config-v1.json b/cloudinit/config/schemas/schema-cloud-config-v1.json index 941eac3e9dc..5fe97a5f840 100644 --- a/cloudinit/config/schemas/schema-cloud-config-v1.json +++ b/cloudinit/config/schemas/schema-cloud-config-v1.json @@ -2068,7 +2068,7 @@ "minItems": 1, "maxItems": 6 }, - "description": "List of lists. Each inner list entry is a list of ``/etc/fstab`` mount declarations of the format: [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno ]. A mount declaration with less than 6 items will get remaining values from **mount_default_fields**. A mount declaration with only `fs_spec` and no `fs_file` mountpoint will be skipped.", + "description": "List of lists. Each inner list entry is a list of ``/etc/fstab`` mount declarations of the format: [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno ]. A mount declaration with less than 6 items will get remaining values from **mount_default_fields**. A mount declaration with only `fs_spec` and no `fs_file` mountpoint will be skipped.", "minItems": 1 }, "mount_default_fields": { diff --git a/doc/examples/cloud-config-mount-points.txt b/doc/examples/cloud-config-mount-points.txt index 2f45fd4d658..a77cdf54a4c 100644 --- a/doc/examples/cloud-config-mount-points.txt +++ b/doc/examples/cloud-config-mount-points.txt @@ -3,7 +3,7 @@ # set up mount points # 'mounts' contains a list of lists # the inner list are entries for an /etc/fstab line -# ie : [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno ] +# ie : [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno ] # # default: # mounts: diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt index e50ba5ac191..92c3395b2a0 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -22,7 +22,7 @@ packages: # set up mount points # 'mounts' contains a list of lists # the inner list are entries for an /etc/fstab line -# ie : [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno ] +# ie : [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno ] # # default: # mounts: diff --git a/doc/module-docs/cc_mounts/data.yaml b/doc/module-docs/cc_mounts/data.yaml index 18193f062d3..7548bddaece 100644 --- a/doc/module-docs/cc_mounts/data.yaml +++ b/doc/module-docs/cc_mounts/data.yaml @@ -3,7 +3,7 @@ cc_mounts: This module can add or remove mount points from ``/etc/fstab`` as well as configure swap. The ``mounts`` config key takes a list of ``fstab`` entries to add. Each entry is specified as a list of ``[ fs_spec, fs_file, - fs_vfstype, fs_mntops, fs-freq, fs_passno ]``. + fs_vfstype, fs_mntops, fs_freq, fs_passno ]``. For more information on these options, consult the manual for ``/etc/fstab``. When specifying the ``fs_spec``, if the device name starts diff --git a/doc/rtd/reference/yaml_examples/mounts.rst b/doc/rtd/reference/yaml_examples/mounts.rst index 4c9534bc68b..42871ca68b8 100644 --- a/doc/rtd/reference/yaml_examples/mounts.rst +++ b/doc/rtd/reference/yaml_examples/mounts.rst @@ -44,7 +44,7 @@ contains entries for an ``/etc/fstab`` line, e.g.: .. code-block:: yaml - [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno ] + [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno ] With defaults: From 37b0e26a5da7e451fcdd0360e58539d7b057b01f Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 3 Jul 2025 16:39:55 +0200 Subject: [PATCH 008/150] test: drop / pytestify ResourceUsingTestCase (#6276) --- .../unittests/config/test_cc_users_groups.py | 33 +++------- tests/unittests/config/test_cc_write_files.py | 11 ++-- .../config/test_cc_write_files_deferred.py | 3 +- tests/unittests/conftest.py | 31 +++++++++- .../distros/package_management/test_apt.py | 4 +- tests/unittests/filters/test_launch_index.py | 33 +++++----- tests/unittests/helpers.py | 61 +------------------ tests/unittests/sources/test_maas.py | 8 +-- tests/unittests/test_data.py | 19 +++--- tests/unittests/test_helpers.py | 19 +++--- tests/unittests/test_merging.py | 20 +++--- tests/unittests/test_util.py | 54 ++++++++-------- 12 files changed, 122 insertions(+), 174 deletions(-) diff --git a/tests/unittests/config/test_cc_users_groups.py b/tests/unittests/config/test_cc_users_groups.py index ec2b18337e5..7327d5b4f0b 100644 --- a/tests/unittests/config/test_cc_users_groups.py +++ b/tests/unittests/config/test_cc_users_groups.py @@ -15,6 +15,7 @@ mock, skipUnlessJsonSchema, ) +from tests.unittests.util import get_cloud MODPATH = "cloudinit.config.cc_users_groups" @@ -39,9 +40,7 @@ def test_handle_no_cfg_creates_no_users_or_groups(self, m_user, m_group): } } metadata = {} - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) m_user.assert_not_called() m_group.assert_not_called() @@ -59,9 +58,7 @@ def test_handle_users_in_cfg_calls_create_users(self, m_user, m_group): } } metadata = {} - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) self.assertCountEqual( m_user.call_args_list, @@ -105,7 +102,7 @@ def test_handle_users_in_cfg_calls_create_users_on_bsd( with mock.patch( "cloudinit.distros.networking.subp.subp", return_value=("", None) ): - cloud = self.tmp_cloud( + cloud = get_cloud( distro="freebsd", sys_cfg=sys_cfg, metadata=metadata ) cc_users_groups.handle("modulename", cfg, cloud, None) @@ -141,9 +138,7 @@ def test_users_with_ssh_redirect_user_passes_keys(self, m_user, m_group): } } metadata = {"public-keys": ["key1"]} - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) self.assertCountEqual( m_user.call_args_list, @@ -182,9 +177,7 @@ def test_users_with_ssh_redirect_user_default_str(self, m_user, m_group): } } metadata = {"public-keys": ["key1"]} - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) self.assertCountEqual( m_user.call_args_list, @@ -216,7 +209,7 @@ def test_users_without_home_cannot_import_ssh_keys(self, m_user, m_group): }, ] } - cloud = self.tmp_cloud(distro="ubuntu", sys_cfg={}, metadata={}) + cloud = get_cloud(distro="ubuntu", sys_cfg={}, metadata={}) with self.assertRaises(ValueError) as context_manager: cc_users_groups.handle("modulename", cfg, cloud, None) m_group.assert_not_called() @@ -244,9 +237,7 @@ def test_users_with_ssh_redirect_user_non_default(self, m_user, m_group): } } metadata = {"public-keys": ["key1"]} - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) with self.assertRaises(ValueError) as context_manager: cc_users_groups.handle("modulename", cfg, cloud, None) m_group.assert_not_called() @@ -269,9 +260,7 @@ def test_users_with_ssh_redirect_user_default_false(self, m_user, m_group): } } metadata = {"public-keys": ["key1"]} - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) self.assertCountEqual( m_user.call_args_list, @@ -295,9 +284,7 @@ def test_users_ssh_redirect_user_and_no_default(self, m_user, m_group): # System config defines *no* default user for the distro. sys_cfg = {} metadata = {} # no public-keys defined - cloud = self.tmp_cloud( - distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata - ) + cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) m_user.assert_called_once_with("me2", default=False) m_group.assert_not_called() diff --git a/tests/unittests/config/test_cc_write_files.py b/tests/unittests/config/test_cc_write_files.py index 7f7f1740112..7cae9a861e4 100644 --- a/tests/unittests/config/test_cc_write_files.py +++ b/tests/unittests/config/test_cc_write_files.py @@ -24,6 +24,7 @@ FilesystemMockingTestCase, skipUnlessJsonSchema, ) +from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) @@ -172,7 +173,7 @@ def test_handle_plain_text(self): } ] } - cc = self.tmp_cloud("ubuntu") + cc = get_cloud("ubuntu") handle("ignored", cfg, cc, []) assert content == util.load_text_file(file_path) self.assertNotIn( @@ -193,7 +194,7 @@ def test_file_uri(self): } ] } - cc = self.tmp_cloud("ubuntu") + cc = get_cloud("ubuntu") handle("ignored", cfg, cc, []) self.assertEqual( util.load_text_file(src_path), util.load_text_file(dst_path) @@ -220,7 +221,7 @@ def test_http_uri(self): } ] } - cc = self.tmp_cloud("ubuntu") + cc = get_cloud("ubuntu") handle("ignored", cfg, cc, []) self.assertEqual(content, util.load_text_file(path)) @@ -240,7 +241,7 @@ def test_uri_fallback(self): } ] } - cc = self.tmp_cloud("ubuntu") + cc = get_cloud("ubuntu") handle("ignored", cfg, cc, []) self.assertEqual(content, util.load_text_file(dst_path)) @@ -248,7 +249,7 @@ def test_deferred(self): self.patchUtils(self.tmp) file_path = "/tmp/deferred.file" config = {"write_files": [{"path": file_path, "defer": True}]} - cc = self.tmp_cloud("ubuntu") + cc = get_cloud("ubuntu") handle("cc_write_file", config, cc, []) with self.assertRaises(FileNotFoundError): util.load_text_file(file_path) diff --git a/tests/unittests/config/test_cc_write_files_deferred.py b/tests/unittests/config/test_cc_write_files_deferred.py index 3fbf800d48f..1a217050cff 100644 --- a/tests/unittests/config/test_cc_write_files_deferred.py +++ b/tests/unittests/config/test_cc_write_files_deferred.py @@ -17,6 +17,7 @@ FilesystemMockingTestCase, skipUnlessJsonSchema, ) +from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) @@ -43,7 +44,7 @@ def test_filtering_deferred_files(self): {"path": "/tmp/not_deferred.file"}, ] } - cc = self.tmp_cloud("ubuntu") + cc = get_cloud("ubuntu") handle("cc_write_files_deferred", config, cc, []) self.assertEqual(util.load_text_file("/tmp/deferred.file"), expected) with self.assertRaises(FileNotFoundError): diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index 3042dee1288..deb765691e5 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -7,7 +7,11 @@ import pytest -from cloudinit import atomic_helper, lifecycle, util +from cloudinit import atomic_helper +from cloudinit import helpers as ch +from cloudinit import lifecycle +from cloudinit import user_data as ud +from cloudinit import util from cloudinit.gpg import GPG from cloudinit.log import loggers from tests.hypothesis import HAS_HYPOTHESIS @@ -170,3 +174,28 @@ def tmp_path(tmpdir): settings.register_profile("ci", max_examples=1000) settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) + + +@pytest.fixture +def MockPaths(tmp_path): + class _MockPaths(ch.Paths): + def __init__(self, path_cfgs: dict, ds=None): + super().__init__(path_cfgs=path_cfgs, ds=ds) + + self.cloud_dir: str = path_cfgs.get( + "cloud_dir", f"{tmp_path}/var/lib/cloud" + ) + self.run_dir: str = path_cfgs.get( + "run_dir", f"{tmp_path}/run/cloud/" + ) + self.template_dir: str = path_cfgs.get( + "templates_dir", f"{tmp_path}/etc/cloud/templates/" + ) + + return _MockPaths + + +@pytest.fixture +def ud_proc(MockPaths): + paths = MockPaths({}) + return ud.UserDataProcessor(paths) diff --git a/tests/unittests/distros/package_management/test_apt.py b/tests/unittests/distros/package_management/test_apt.py index 00523873f3b..8f333be3d38 100644 --- a/tests/unittests/distros/package_management/test_apt.py +++ b/tests/unittests/distros/package_management/test_apt.py @@ -8,7 +8,6 @@ from cloudinit import helpers, subp from cloudinit.distros.package_management import apt from cloudinit.distros.package_management.apt import APT_GET_COMMAND, Apt -from tests.unittests.helpers import get_mock_paths from tests.unittests.util import FakeDataSource M_PATH = "cloudinit.distros.package_management.apt.Apt." @@ -120,8 +119,7 @@ def test_search_stem(self, m_subp, m_which, mocker): @pytest.fixture(scope="function") -def apt_paths(tmpdir): - MockPaths = get_mock_paths(str(tmpdir)) +def apt_paths(MockPaths, tmpdir): with mock.patch.object( apt, "APT_LOCK_FILES", diff --git a/tests/unittests/filters/test_launch_index.py b/tests/unittests/filters/test_launch_index.py index 1b2ebfb2e20..7e8895b49b6 100644 --- a/tests/unittests/filters/test_launch_index.py +++ b/tests/unittests/filters/test_launch_index.py @@ -18,15 +18,15 @@ def count_messages(root): return am -class TestLaunchFilter(helpers.ResourceUsingTestCase): +class TestLaunchFilter: def assertCounts(self, message, expected_counts): orig_message = copy.deepcopy(message) for index, count in expected_counts.items(): index = util.safe_int(index) filtered_message = launch_index.Filter(index).apply(message) - self.assertEqual(count_messages(filtered_message), count) + assert count_messages(filtered_message) == count # Ensure original message still ok/not modified - self.assertTrue(self.equivalentMessage(message, orig_message)) + assert self.equivalentMessage(message, orig_message) is True def equivalentMessage(self, msg1, msg2): msg1_count = count_messages(msg1) @@ -51,11 +51,10 @@ def equivalentMessage(self, msg1, msg2): return False return True - def testMultiEmailIndex(self): + def testMultiEmailIndex(self, ud_proc): test_data = helpers.readResource("filter_cloud_multipart_2.email") - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(test_data) - self.assertTrue(count_messages(message) > 0) + assert count_messages(message) > 0 # This file should have the following # indexes -> amount mapping in it expected_counts = { @@ -66,11 +65,10 @@ def testMultiEmailIndex(self): } self.assertCounts(message, expected_counts) - def testHeaderEmailIndex(self): + def testHeaderEmailIndex(self, ud_proc): test_data = helpers.readResource("filter_cloud_multipart_header.email") - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(test_data) - self.assertTrue(count_messages(message) > 0) + assert count_messages(message) > 0 # This file should have the following # indexes -> amount mapping in it expected_counts = { @@ -81,11 +79,10 @@ def testHeaderEmailIndex(self): } self.assertCounts(message, expected_counts) - def testConfigEmailIndex(self): + def testConfigEmailIndex(self, ud_proc): test_data = helpers.readResource("filter_cloud_multipart_1.email") - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(test_data) - self.assertTrue(count_messages(message) > 0) + assert count_messages(message) > 0 # This file should have the following # indexes -> amount mapping in it expected_counts = { @@ -95,21 +92,19 @@ def testConfigEmailIndex(self): } self.assertCounts(message, expected_counts) - def testNoneIndex(self): + def testNoneIndex(self, ud_proc): test_data = helpers.readResource("filter_cloud_multipart.yaml") - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(test_data) start_count = count_messages(message) - self.assertTrue(start_count > 0) + assert start_count > 0 filtered_message = launch_index.Filter(None).apply(message) - self.assertTrue(self.equivalentMessage(message, filtered_message)) + assert self.equivalentMessage(message, filtered_message) - def testIndexes(self): + def testIndexes(self, ud_proc): test_data = helpers.readResource("filter_cloud_multipart.yaml") - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(test_data) start_count = count_messages(message) - self.assertTrue(start_count > 0) + assert start_count > 0 # This file should have the following # indexes -> amount mapping in it expected_counts = { diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index dfd9a508c25..e443b8b8c16 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -20,14 +20,11 @@ import responses -from cloudinit import atomic_helper, cloud, distros -from cloudinit import helpers as ch -from cloudinit import subp, util +from cloudinit import atomic_helper, subp, util from cloudinit.config.schema import ( SchemaValidationError, validate_cloudconfig_schema, ) -from cloudinit.sources import DataSourceNone from cloudinit.templater import JINJA_AVAILABLE from tests.helpers import cloud_init_project_dir from tests.hypothesis_jsonschema import HAS_HYPOTHESIS_JSONSCHEMA @@ -262,53 +259,19 @@ def tmp_path(self, path, dir=None): dir = self.tmp_dir() return os.path.normpath(os.path.abspath(os.path.join(dir, path))) - def tmp_cloud(self, distro, sys_cfg=None, metadata=None): - """Create a cloud with tmp working directory paths. - - @param distro: Name of the distro to attach to the cloud. - @param metadata: Optional metadata to set on the datasource. - - @return: The built cloud instance. - """ - self.new_root = self.tmp_dir() - if not sys_cfg: - sys_cfg = {} - MockPaths = get_mock_paths(self.new_root) - self.paths = MockPaths({}) - cls = distros.fetch(distro) - mydist = cls(distro, sys_cfg, self.paths) - myds = DataSourceNone.DataSourceNone(sys_cfg, mydist, self.paths) - if metadata: - myds.metadata.update(metadata) - return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None) - @classmethod def random_string(cls, length=8): return random_string(length) -class ResourceUsingTestCase(CiTestCase): - def setUp(self): - super(ResourceUsingTestCase, self).setUp() - self.resource_path = None - - def getCloudPaths(self, ds=None): - tmpdir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmpdir) - cp = ch.Paths( - {"cloud_dir": tmpdir, "templates_dir": resourceLocation()}, ds=ds - ) - return cp - - -class FilesystemMockingTestCase(ResourceUsingTestCase): +class FilesystemMockingTestCase(CiTestCase): def setUp(self): super(FilesystemMockingTestCase, self).setUp() self.patched_funcs = ExitStack() def tearDown(self): self.patched_funcs.close() - ResourceUsingTestCase.tearDown(self) + CiTestCase.tearDown(self) def replicateTestRoot(self, example_root, target_root): real_root = resourceLocation() @@ -461,24 +424,6 @@ def _ensure_url_default_path(url): ) -def get_mock_paths(temp_dir): - class MockPaths(ch.Paths): - def __init__(self, path_cfgs: dict, ds=None): - super().__init__(path_cfgs=path_cfgs, ds=ds) - - self.cloud_dir: str = path_cfgs.get( - "cloud_dir", f"{temp_dir}/var/lib/cloud" - ) - self.run_dir: str = path_cfgs.get( - "run_dir", f"{temp_dir}/run/cloud/" - ) - self.template_dir: str = path_cfgs.get( - "templates_dir", f"{temp_dir}/etc/cloud/templates/" - ) - - return MockPaths - - class ResponsesTestCase(CiTestCase): def setUp(self): super().setUp() diff --git a/tests/unittests/sources/test_maas.py b/tests/unittests/sources/test_maas.py index 5d31c916d9f..9f0dbb34884 100644 --- a/tests/unittests/sources/test_maas.py +++ b/tests/unittests/sources/test_maas.py @@ -9,7 +9,7 @@ from cloudinit import helpers, settings, url_helper from cloudinit.sources import DataSourceMAAS -from tests.unittests.helpers import get_mock_paths, populate_dir +from tests.unittests.helpers import populate_dir from tests.unittests.util import MockDistro @@ -224,7 +224,7 @@ def tests_wb_local_stage_detects_datasource_on_initramfs_network( assert expected == ds.get_data() @responses.activate - def test_get_data_with_retry(self, mocker, tmp_path, caplog): + def test_get_data_with_retry(self, mocker, MockPaths, caplog): """Ensure we can get data from IMDS even if some attempts fail.""" mocker.patch("time.sleep") metadata_url = "http://169.254.169.254/MAAS/metadata" @@ -260,9 +260,7 @@ def test_get_data_with_retry(self, mocker, tmp_path, caplog): ) cfg = {"datasource": {"MAAS": {"metadata_url": metadata_url}}} - ds = DataSourceMAAS.DataSourceMAAS( - cfg, MockDistro(), get_mock_paths(tmp_path)({}) - ) + ds = DataSourceMAAS.DataSourceMAAS(cfg, MockDistro(), MockPaths({})) assert ds.get_data() assert ds.metadata["instance-id"] == "i-123" assert ds.metadata["local-hostname"] == "myhostname" diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index 0e26ba51f12..0184d4636c0 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -770,26 +770,21 @@ def test_include_bad_url_no_fail( assert cc.get("included") is True -class TestUDProcess(helpers.ResourceUsingTestCase): - def test_bytes_in_userdata(self): +class TestUDProcess: + def test_bytes_in_userdata(self, ud_proc): msg = b"#cloud-config\napt_update: True\n" - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(msg) - self.assertTrue(count_messages(message) == 1) + assert count_messages(message) == 1 - def test_string_in_userdata(self): + def test_string_in_userdata(self, ud_proc): msg = "#cloud-config\napt_update: True\n" - - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(msg) - self.assertTrue(count_messages(message) == 1) + assert count_messages(message) == 1 - def test_compressed_in_userdata(self): + def test_compressed_in_userdata(self, ud_proc): msg = gzip_text("#cloud-config\napt_update: True\n") - - ud_proc = ud.UserDataProcessor(self.getCloudPaths()) message = ud_proc.process(msg) - self.assertTrue(count_messages(message) == 1) + assert count_messages(message) == 1 class TestConvertString(helpers.TestCase): diff --git a/tests/unittests/test_helpers.py b/tests/unittests/test_helpers.py index 3ef92beca75..cd81745067d 100644 --- a/tests/unittests/test_helpers.py +++ b/tests/unittests/test_helpers.py @@ -7,7 +7,6 @@ from cloudinit import sources from tests.helpers import cloud_init_project_dir, get_top_level_dir -from tests.unittests.helpers import ResourceUsingTestCase class MyDataSource(sources.DataSource): @@ -17,24 +16,24 @@ def get_instance_id(self): return self._instance_id -class TestPaths(ResourceUsingTestCase): - def test_get_ipath_and_instance_id_with_slashes(self): +class TestPaths: + def test_get_ipath_and_instance_id_with_slashes(self, MockPaths): myds = MyDataSource(sys_cfg={}, distro=None, paths={}) myds._instance_id = "/foo/bar" safe_iid = "_foo_bar" - mypaths = self.getCloudPaths(myds) + mypaths = MockPaths({}, myds) - self.assertEqual( - os.path.join(mypaths.cloud_dir, "instances", safe_iid), - mypaths.get_ipath(), + assert ( + os.path.join(mypaths.cloud_dir, "instances", safe_iid) + == mypaths.get_ipath() ) - def test_get_ipath_and_empty_instance_id_returns_none(self): + def test_get_ipath_and_empty_instance_id_returns_none(self, MockPaths): myds = MyDataSource(sys_cfg={}, distro=None, paths={}) myds._instance_id = None - mypaths = self.getCloudPaths(myds) + mypaths = MockPaths({}, myds) - self.assertIsNone(mypaths.get_ipath()) + assert mypaths.get_ipath() is None class Testcloud_init_project_dir: diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py index efb71618ce3..369f3b3113e 100644 --- a/tests/unittests/test_merging.py +++ b/tests/unittests/test_merging.py @@ -100,7 +100,7 @@ def make_dict(max_depth, seed=None): return _make_dict(0, max_depth, rand) -class TestSimpleRun(helpers.ResourceUsingTestCase): +class TestSimpleRun: def _load_merge_files(self): merge_root = helpers.resourceLocation("merge_sources") tests = [] @@ -138,7 +138,7 @@ def test_seed_runs(self): for test in test_dicts: c = _old_mergemanydict(*test) d = util.mergemanydict(test) - self.assertEqual(c, d) + assert c == d def test_merge_cc_samples(self): tests = self._load_merge_files() @@ -162,7 +162,7 @@ def test_merge_cc_samples(self): merged_buf, expected_merge, ) - self.assertEqual(expected_merge, merged_buf, msg=fail_msg) + assert expected_merge == merged_buf, fail_msg def test_compat_merges_dict(self): a = { @@ -174,7 +174,7 @@ def test_compat_merges_dict(self): } c = _old_mergedict(a, b) d = util.mergemanydict([a, b]) - self.assertEqual(c, d) + assert c == d def test_compat_merges_dict2(self): a = { @@ -189,7 +189,7 @@ def test_compat_merges_dict2(self): } c = _old_mergedict(a, b) d = util.mergemanydict([a, b]) - self.assertEqual(c, d) + assert c == d def test_compat_merges_list(self): a = {"b": [1, 2, 3]} @@ -197,7 +197,7 @@ def test_compat_merges_list(self): c = {"b": [6, 7]} e = _old_mergemanydict(a, b, c) f = util.mergemanydict([a, b, c]) - self.assertEqual(e, f) + assert e == f def test_compat_merges_str(self): a = {"b": "hi"} @@ -205,7 +205,7 @@ def test_compat_merges_str(self): c = {"b": "hallo"} e = _old_mergemanydict(a, b, c) f = util.mergemanydict([a, b, c]) - self.assertEqual(e, f) + assert e == f def test_compat_merge_sub_dict(self): a = { @@ -229,7 +229,7 @@ def test_compat_merge_sub_dict(self): } c = _old_mergedict(a, b) d = util.mergemanydict([a, b]) - self.assertEqual(c, d) + assert c == d def test_compat_merge_sub_dict2(self): a = { @@ -245,7 +245,7 @@ def test_compat_merge_sub_dict2(self): } c = _old_mergedict(a, b) d = util.mergemanydict([a, b]) - self.assertEqual(c, d) + assert c == d def test_compat_merge_sub_list(self): a = { @@ -261,7 +261,7 @@ def test_compat_merge_sub_list(self): } c = _old_mergedict(a, b) d = util.mergemanydict([a, b]) - self.assertEqual(c, d) + assert c == d class TestMergingSchema: diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index 1a6bc75447e..3d681939d14 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -2171,7 +2171,7 @@ def test_none_returns_default(self): ) -class TestMountinfoParsing(helpers.ResourceUsingTestCase): +class TestMountinfoParsing: def test_invalid_mountinfo(self): line = ( "20 1 252:1 / / rw,relatime - ext4 /dev/mapper/vg0-root" @@ -2184,49 +2184,49 @@ def test_invalid_mountinfo(self): expected = None else: expected = ("/dev/mapper/vg0-root", "ext4", "/") - self.assertEqual(expected, util.parse_mount_info("/", lines)) + assert expected == util.parse_mount_info("/", lines) def test_precise_ext4_root(self): lines = helpers.readResource("mountinfo_precise_ext4.txt").splitlines() expected = ("/dev/mapper/vg0-root", "ext4", "/") - self.assertEqual(expected, util.parse_mount_info("/", lines)) - self.assertEqual(expected, util.parse_mount_info("/usr", lines)) - self.assertEqual(expected, util.parse_mount_info("/usr/bin", lines)) + assert expected == util.parse_mount_info("/", lines) + assert expected == util.parse_mount_info("/usr", lines) + assert expected == util.parse_mount_info("/usr/bin", lines) expected = ("/dev/md0", "ext4", "/boot") - self.assertEqual(expected, util.parse_mount_info("/boot", lines)) - self.assertEqual(expected, util.parse_mount_info("/boot/grub", lines)) + assert expected == util.parse_mount_info("/boot", lines) + assert expected == util.parse_mount_info("/boot/grub", lines) expected = ("/dev/mapper/vg0-root", "ext4", "/") - self.assertEqual(expected, util.parse_mount_info("/home", lines)) - self.assertEqual(expected, util.parse_mount_info("/home/me", lines)) + assert expected == util.parse_mount_info("/home", lines) + assert expected == util.parse_mount_info("/home/me", lines) expected = ("tmpfs", "tmpfs", "/run") - self.assertEqual(expected, util.parse_mount_info("/run", lines)) + assert expected == util.parse_mount_info("/run", lines) expected = ("none", "tmpfs", "/run/lock") - self.assertEqual(expected, util.parse_mount_info("/run/lock", lines)) + assert expected == util.parse_mount_info("/run/lock", lines) def test_raring_btrfs_root(self): lines = helpers.readResource("mountinfo_raring_btrfs.txt").splitlines() expected = ("/dev/vda1", "btrfs", "/") - self.assertEqual(expected, util.parse_mount_info("/", lines)) - self.assertEqual(expected, util.parse_mount_info("/usr", lines)) - self.assertEqual(expected, util.parse_mount_info("/usr/bin", lines)) - self.assertEqual(expected, util.parse_mount_info("/boot", lines)) - self.assertEqual(expected, util.parse_mount_info("/boot/grub", lines)) + assert expected == util.parse_mount_info("/", lines) + assert expected == util.parse_mount_info("/usr", lines) + assert expected == util.parse_mount_info("/usr/bin", lines) + assert expected == util.parse_mount_info("/boot", lines) + assert expected == util.parse_mount_info("/boot/grub", lines) expected = ("/dev/vda1", "btrfs", "/home") - self.assertEqual(expected, util.parse_mount_info("/home", lines)) - self.assertEqual(expected, util.parse_mount_info("/home/me", lines)) + assert expected == util.parse_mount_info("/home", lines) + assert expected == util.parse_mount_info("/home/me", lines) expected = ("tmpfs", "tmpfs", "/run") - self.assertEqual(expected, util.parse_mount_info("/run", lines)) + assert expected == util.parse_mount_info("/run", lines) expected = ("none", "tmpfs", "/run/lock") - self.assertEqual(expected, util.parse_mount_info("/run/lock", lines)) + assert expected == util.parse_mount_info("/run/lock", lines) @mock.patch("cloudinit.subp.subp") def test_parse_mount_with_ext(self, mount_out): @@ -2236,16 +2236,16 @@ def test_parse_mount_with_ext(self, mount_out): ) # this one is valid and exists in mount_parse_ext.txt ret = util.parse_mount("/var") - self.assertEqual(("/dev/mapper/vg00-lv_var", "ext4", "/var"), ret) + assert ("/dev/mapper/vg00-lv_var", "ext4", "/var") == ret # another one that is valid and exists ret = util.parse_mount("/") - self.assertEqual(("/dev/mapper/vg00-lv_root", "ext4", "/"), ret) + assert ("/dev/mapper/vg00-lv_root", "ext4", "/") == ret # this one exists in mount_parse_ext.txt ret = util.parse_mount("/sys/kernel/debug") - self.assertEqual(("none", "debugfs", "/sys/kernel/debug"), ret) + assert ("none", "debugfs", "/sys/kernel/debug") == ret # this one does not exist in mount_parse_ext.txt ret = util.parse_mount("/var/tmp/cloud-init") - self.assertEqual(("/dev/mapper/vg00-lv_var", "ext4", "/var"), ret) + assert ("/dev/mapper/vg00-lv_var", "ext4", "/var") == ret @mock.patch("cloudinit.subp.subp") def test_parse_mount_with_zfs(self, mount_out): @@ -2255,13 +2255,13 @@ def test_parse_mount_with_zfs(self, mount_out): ) # this one is valid and exists in mount_parse_zfs.txt ret = util.parse_mount("/var") - self.assertEqual(("vmzroot/ROOT/freebsd/var", "zfs", "/var"), ret) + assert ("vmzroot/ROOT/freebsd/var", "zfs", "/var") == ret # this one is the root, valid and also exists in mount_parse_zfs.txt ret = util.parse_mount("/") - self.assertEqual(("vmzroot/ROOT/freebsd", "zfs", "/"), ret) + assert ("vmzroot/ROOT/freebsd", "zfs", "/") == ret # this one does not exist in mount_parse_ext.txt ret = util.parse_mount("/var/tmp/cloud-init") - self.assertEqual(("vmzroot/var/tmp", "zfs", "/var/tmp"), ret) + assert ("vmzroot/var/tmp", "zfs", "/var/tmp") == ret class TestIsX86(helpers.CiTestCase): From ab2cc28d01d1cbac3bccbf5df54f1c2464838d74 Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Thu, 3 Jul 2025 19:15:06 -0300 Subject: [PATCH 009/150] feat(wsl): Reuse metadata as Landscape installation_request_id (#6200) Recently the Landscape server started tracking the status of commands delivered to clients that run on parent instances, notably the Ubuntu Pro for WSL agent, by assigning request IDs which the pro agent refers to when replying with the command completion status. For the case of the Install command, which results in a new WSL instance being created on the parent Windows machine and registered with Landscape, the server also expects the same value to be part of the landscape-client registration message. For that we need the `installation_request_id` to be added to the client.conf file. We found that synergic to the concept of `metadata.instance-id`, thus this refactoring in the WSL datasource aims to allow: - the pro agent to write a per-instance metadata file - the WSL data source to use the `metadata.instance-id` as `installation_request_id` - if the landscape.client config exists - if doesn't contain that field. Details were discussed in the specification document WS042 - Handle Landscape request_id in command Install via cloud-init --- cloudinit/sources/DataSourceWSL.py | 129 ++++++++++++++++++++------ doc/rtd/reference/datasources/wsl.rst | 17 ++-- tests/unittests/sources/test_wsl.py | 75 +++++++++++++-- 3 files changed, 176 insertions(+), 45 deletions(-) diff --git a/cloudinit/sources/DataSourceWSL.py b/cloudinit/sources/DataSourceWSL.py index f99ecb5c432..e51db4eee60 100644 --- a/cloudinit/sources/DataSourceWSL.py +++ b/cloudinit/sources/DataSourceWSL.py @@ -25,6 +25,9 @@ DEFAULT_INSTANCE_ID = "iid-datasource-wsl" LANDSCAPE_DATA_FILE = "%s.user-data" AGENT_DATA_FILE = "agent.yaml" +LANDSCAPE_CFG_KEY = "landscape" +LANDSCAPE_CLIENT_CFG_KEY = "client" +LANDSCAPE_INSTALLATION_REQ_ID = "installation_request_id" def instance_name() -> str: @@ -148,7 +151,10 @@ class ConfigData: """Models a piece of configuration data as a dict if possible, while retaining its raw representation alongside its file path""" - def __init__(self, path: PurePath): + def is_cloud_config(self) -> bool: + return self.config_dict is not None + + def __init__(self, path: PurePath, instance_id: str): self.raw: str = util.load_text_file(path) self.path: PurePath = path @@ -157,35 +163,73 @@ def __init__(self, path: PurePath): if "text/cloud-config" == type_from_starts_with(self.raw): self.config_dict = util.load_yaml(self.raw) - def is_cloud_config(self) -> bool: - return self.config_dict is not None + if ( + self.config_dict # Valid non-empty config + and instance_id + and instance_id != DEFAULT_INSTANCE_ID # custom instance-id + and self.config_dict.get(LANDSCAPE_CFG_KEY, {}).get( + LANDSCAPE_CLIENT_CFG_KEY + ) + is not None # Landscape client config exists + ): + # Let's set the installation_request_id to the metadata.instance-id + # if not already set, and update the raw config if we modified it: + req_id = self.config_dict[LANDSCAPE_CFG_KEY][ + LANDSCAPE_CLIENT_CFG_KEY + ].setdefault(LANDSCAPE_INSTALLATION_REQ_ID, instance_id) + if req_id == instance_id: + self.raw = ( + "#cloud-config\n%s" % yaml.dump(self.config_dict).strip() + ) -def load_instance_metadata( - cloudinitdir: Optional[PurePath], instance_name: str -) -> dict: +def _load_metadata(metadata_path: PurePath) -> Optional[dict]: """ - Returns the relevant metadata loaded from cloudinit dir based on the - instance name + Returns the relevant metadata loaded from the supplied metadata_path. """ - metadata = {"instance-id": DEFAULT_INSTANCE_ID} - if cloudinitdir is None: - return metadata - metadata_path = os.path.join( - cloudinitdir.as_posix(), "%s.meta-data" % instance_name - ) - + metadata = None try: - metadata = util.load_yaml(util.load_text_file(metadata_path)) + metadata = util.load_yaml( + util.load_text_file(metadata_path), default={} + ) except FileNotFoundError: LOG.debug( - "No instance metadata found at %s. Using default instance-id.", + "No instance metadata found at %s.", metadata_path, ) + + return metadata + + +def load_instance_metadata(user_home: PurePath, instance_name: str) -> dict: + """ + Returns the relevant metadata loaded from either the Ubuntu Pro dir + or the user cloud-int dir based on the instance name + """ + metadata = {"instance-id": DEFAULT_INSTANCE_ID} + pro_dir = cloud_init_data_dir(user_home / ".ubuntupro") + user_dir = cloud_init_data_dir(user_home) + found_at = "" + for dir in [pro_dir, user_dir]: + if dir is None: + continue + path = dir / ("%s.meta-data" % instance_name) + dt = _load_metadata(path) + if dt is not None: + metadata = dt + found_at = path.as_posix() + break + + if not found_at: + LOG.debug( + "Unable to find meta-data file candidates." + " Using default instance-id" + ) + if not metadata or "instance-id" not in metadata: # Parsed metadata file invalid msg = ( - f" Metadata at {metadata_path} does not contain instance-id key." + f"Metadata at {found_at} does not contain instance-id key." f" Instead received: {metadata}" ) LOG.error(msg) @@ -195,14 +239,14 @@ def load_instance_metadata( def load_ubuntu_pro_data( - user_home: PurePath, + user_home: PurePath, instance_id: str ) -> Tuple[Optional[ConfigData], Optional[ConfigData]]: """ Read .ubuntupro user-data if present and return a tuple of agent and landscape user-data. """ - pro_dir = os.path.join(user_home, ".ubuntupro/.cloud-init") - if not os.path.isdir(pro_dir): + pro_dir = cloud_init_data_dir(user_home / ".ubuntupro") + if pro_dir is None: return None, None landscape_path = PurePath( @@ -216,12 +260,12 @@ def load_ubuntu_pro_data( landscape_path, cloud_init_data_dir(user_home), ) - landscape_data = ConfigData(landscape_path) + landscape_data = ConfigData(landscape_path, instance_id) agent_path = PurePath(os.path.join(pro_dir, AGENT_DATA_FILE)) agent_data = None if os.path.isfile(agent_path): - agent_data = ConfigData(agent_path) + agent_data = ConfigData(agent_path, instance_id) return agent_data, landscape_data @@ -311,6 +355,27 @@ def merge_agent_landscape_data( ) +def landscape_supports_field(field: str): + """Checks if the landscape-config binary supports the intended + configuration field, returning False if not or if attempting + to run that binary fails. + """ + try: + flag = "--" + field.replace("_", "-") + # landscape-config is the command that understand the config fields, + # not landscape-client itself. + out, _ = subp.subp(["landscape-config", "--help"]) + return flag in out + + except subp.ProcessExecutionError as err: + LOG.warning( + "Unable to verify if landscape-client supports %s: %s", + field, + err.reason, + ) + return False + + class DataSourceWSL(sources.DataSource): dsname = "WSL" @@ -356,8 +421,8 @@ def check_instance_id(self, sys_cfg) -> bool: return False try: - data_dir = cloud_init_data_dir(find_home()) - metadata = load_instance_metadata(data_dir, instance_name()) + home = find_home() + metadata = load_instance_metadata(home, instance_name()) return current == metadata.get("instance-id") except (IOError, ValueError) as err: @@ -389,20 +454,26 @@ def _get_data(self) -> bool: # Load any metadata try: self.metadata = load_instance_metadata( - seed_dir, self.instance_name + user_home, self.instance_name ) except (ValueError, IOError) as err: LOG.error("Unable to load metadata: %s", str(err)) return False - # # Load Ubuntu Pro configs only on Ubuntu distros + iid = self.metadata["instance-id"] + # Load Ubuntu Pro configs only on Ubuntu distros if self.distro.name == "ubuntu": - agent_data, user_data = load_ubuntu_pro_data(user_home) + if not landscape_supports_field(LANDSCAPE_INSTALLATION_REQ_ID): + # Prevents reusing metadata.instance-id as + # landscape.installation_request_id if the + # current version of landscape_client doesn't support it. + iid = "" + agent_data, user_data = load_ubuntu_pro_data(user_home, iid) # Load regular user configs try: if user_data is None and seed_dir is not None: - user_data = ConfigData(self.find_user_data_file(seed_dir)) + user_data = ConfigData(self.find_user_data_file(seed_dir), "") except (ValueError, IOError) as err: log = LOG.info if agent_data else LOG.error diff --git a/doc/rtd/reference/datasources/wsl.rst b/doc/rtd/reference/datasources/wsl.rst index 22d21252cfb..bed96420947 100644 --- a/doc/rtd/reference/datasources/wsl.rst +++ b/doc/rtd/reference/datasources/wsl.rst @@ -135,14 +135,15 @@ the case to the best of our knowledge at the time of this writing. Most of what ``meta-data`` is intended for is not applicable under WSL, such as setting a hostname. Yet, the knowledge of ``meta-data.instance-id`` is vital for cloud-init. So, this datasource provides a default value but also supports -optionally sourcing meta-data from a per-instance specific configuration file: -``%USERPROFILE%\.cloud-init\.meta-data``. If that file exists, it -is a YAML-formatted file minimally providing a value for instance ID -such as: ``instance-id: x-y-z``. Advanced users looking to share -snapshots or relaunch a snapshot where cloud-init is re-triggered, must run -``sudo cloud-init clean --logs`` on the instance before snapshot/export, or -create the appropriate ``.meta-data`` file containing ``instance-id: -some-new-instance-id``. +optionally sourcing meta-data from a per-instance specific configuration file +located either at ``%USERPROFILE%\.cloud-init\.meta-data`` or +``%USERPROFILE%\.ubuntupro\.cloud-init\.meta-data``. When both +files exist, only the second will be loaded. The file must be a YAML-formatted +file minimally providing a value for instance ID such as: ``instance-id: +x-y-z``. Advanced users looking to share snapshots or relaunch a snapshot where +cloud-init is re-triggered, must run ``sudo cloud-init clean --logs`` on the +instance before snapshot/export, or create the appropriate ``.meta-data`` file +containing ``instance-id: some-new-instance-id``. Unsupported or restricted modules and features =============================================== diff --git a/tests/unittests/sources/test_wsl.py b/tests/unittests/sources/test_wsl.py index ebe01827b77..63ca0d1ae5d 100644 --- a/tests/unittests/sources/test_wsl.py +++ b/tests/unittests/sources/test_wsl.py @@ -182,18 +182,29 @@ def test_candidate_files(self, m_gld, linux_distro_value, files): assert files == wsl.candidate_user_data_file_names(INSTANCE_NAME) @pytest.mark.parametrize( - "md_content,raises,errors,warnings,md_expected", + "md_content,is_from_pro,raises,errors,warnings,md_expected", ( pytest.param( None, + False, does_not_raise(), [], [], {"instance-id": "iid-datasource-wsl"}, id="default_md_on_no_md_file", ), + pytest.param( + '{"instance-id":"iid-load-from-pro"}', + True, + does_not_raise(), + [], + [], + {"instance-id": "iid-load-from-pro"}, + id="metadata_from_pro", + ), pytest.param( "{}", + False, pytest.raises( ValueError, match=( @@ -207,6 +218,7 @@ def test_candidate_files(self, m_gld, linux_distro_value, files): ), pytest.param( "{", + True, pytest.raises( ValueError, match=( @@ -221,15 +233,29 @@ def test_candidate_files(self, m_gld, linux_distro_value, files): ), ) def test_load_instance_metadata( - self, md_content, raises, errors, warnings, md_expected, tmpdir, caplog + self, + md_content, + is_from_pro, + raises, + errors, + warnings, + md_expected, + tmpdir, + caplog, ): """meta-data file is optional. Errors are raised on invalid content.""" + path = ".cloud-init" + if is_from_pro: + path = ".ubuntupro/.cloud-init" + if md_content is not None: - tmpdir.join("myinstance.meta-data").write(md_content) + dir = tmpdir.join(path) + os.makedirs(dir) + dir.join("myinstance.meta-data").write(md_content) with caplog.at_level(logging.WARNING): with raises: assert md_expected == wsl.load_instance_metadata( - PurePath(tmpdir), "myinstance" + tmpdir, "myinstance" ) warning_logs = "\n".join( [ @@ -256,6 +282,28 @@ def test_load_instance_metadata( else: assert "" == error_logs + @mock.patch("cloudinit.util.subp.subp") + def test_landscape_supports_field(self, m_subp): + LANDSCAPE_HELP_OUTPUT = """\ + Usage: landscape-config [options] + + Options: + --version show program's version number and exit + -h, --help show this help message and exit + --installation-request-id Only set this value if this computer is a + instance managed by Landscape, in which + it to be the request id that Landscape + to the installation activity for the host + --access-group=ACCESS_GROUP + Suggested access group for this computer. + --tags=TAGS Comma separated list of tag names to be sent + to the server. + """ + m_subp.return_value = util.subp.SubpResult(LANDSCAPE_HELP_OUTPUT, "") + assert wsl.landscape_supports_field("non_sense") is False + assert wsl.landscape_supports_field("tags") is True + assert wsl.landscape_supports_field("installation-request-id") is True + SAMPLE_CFG = {"datasource_list": ["NoCloud", "WSL"]} @@ -317,11 +365,11 @@ def test_merged_data_excludes_empty_or_none( if agent_yaml is not None: agent_path = tmpdir.join("agent.yaml") agent_path.write(agent_yaml) - agent_data = wsl.ConfigData(agent_path) + agent_data = wsl.ConfigData(agent_path, "") if landscape_user_data is not None: landscape_ud_path = tmpdir.join("instance_name.user_data") landscape_ud_path.write(landscape_user_data) - user_data = wsl.ConfigData(landscape_ud_path) + user_data = wsl.ConfigData(landscape_ud_path, "") assert expected == wsl.merge_agent_landscape_data( agent_data, user_data ) @@ -342,6 +390,10 @@ def setup(self, mocker, tmpdir): "cloudinit.sources.DataSourceWSL.subp.which", return_value="/usr/bin/wslpath", ) + mocker.patch( + "cloudinit.sources.DataSourceWSL.landscape_supports_field", + return_value=True, + ) def test_metadata_id_default(self, tmpdir, paths): """ @@ -551,9 +603,9 @@ def test_data_precedence(self, m_get_linux_dist, tmpdir, paths): def test_interaction_with_pro(self, m_get_linux_dist, tmpdir, paths): """Validates the interaction of user-data and Pro For WSL agent data""" - m_get_linux_dist.return_value = SAMPLE_LINUX_DISTRO + m_get_linux_dist.return_value = ("ubuntu", "25.10", "plucky") - user_file = tmpdir.join(".cloud-init", "ubuntu-24.04.user-data") + user_file = tmpdir.join(".cloud-init", "ubuntu-25.10.user-data") user_file.dirpath().mkdir() user_file.write("#cloud-config\nwrite_files:\n- path: /etc/wsl.conf") @@ -575,6 +627,11 @@ def test_interaction_with_pro(self, m_get_linux_dist, tmpdir, paths): ubuntu_pro: token: testtoken""" ) + SAMPLE_ID = "Nice-ID" + agent_metadata_path = ubuntu_pro_tmp.join(f"{INSTANCE_NAME}.meta-data") + agent_metadata_path.write( + f'{{"instance-id":"{SAMPLE_ID}"}}', + ) # Run the datasource ds = wsl.DataSourceWSL( @@ -596,6 +653,8 @@ def test_interaction_with_pro(self, m_get_linux_dist, tmpdir, paths): assert "ubuntu_pro" in userdata assert "landscape" in userdata assert "agenttest" in userdata + assert "installation_request_id" in userdata + assert SAMPLE_ID in userdata @mock.patch("cloudinit.util.get_linux_distro") def test_landscape_vs_local_user(self, m_get_linux_dist, tmpdir, paths): From 1dfd7b59738fffd7c9ab5fe405f556e7b8143109 Mon Sep 17 00:00:00 2001 From: Kees Bakker Date: Fri, 4 Jul 2025 14:36:11 +0200 Subject: [PATCH 010/150] feat(ca_certs): Add CentOS support (#6287) CentOS is using the same PKI paths as RHEL. Signed-off-by: Kees Bakker --- cloudinit/config/cc_ca_certs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py index 67250a05645..4b89977fd1e 100644 --- a/cloudinit/config/cc_ca_certs.py +++ b/cloudinit/config/cc_ca_certs.py @@ -72,6 +72,7 @@ for distro in ( "almalinux", + "centos", "cloudlinux", "rocky", ): @@ -80,6 +81,7 @@ distros = [ "almalinux", "aosc", + "centos", "cloudlinux", "alpine", "debian", From a113049d0408351a8f01074d7131f8cd17036b3b Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab <46541787+mostafaCamel@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:54:02 +0300 Subject: [PATCH 011/150] test: pytestify test_vmware.py (#6288) Signed-off-by: Mostafa Abdelwahab --- tests/unittests/sources/test_vmware.py | 851 +++++++++++-------------- 1 file changed, 361 insertions(+), 490 deletions(-) diff --git a/tests/unittests/sources/test_vmware.py b/tests/unittests/sources/test_vmware.py index eee5c9bcfee..dacc0a5a060 100644 --- a/tests/unittests/sources/test_vmware.py +++ b/tests/unittests/sources/test_vmware.py @@ -9,22 +9,17 @@ import gzip import os from contextlib import ExitStack +from logging import DEBUG from textwrap import dedent import pytest -from cloudinit import dmi, helpers, safeyaml, settings, util +from cloudinit import dmi, safeyaml, settings, util from cloudinit.event import EventScope from cloudinit.sources import DataSourceVMware from cloudinit.sources.helpers.vmware.imc import guestcust_util from cloudinit.subp import ProcessExecutionError -from tests.unittests.helpers import ( - CiTestCase, - FilesystemMockingTestCase, - mock, - populate_dir, - wrap_and_call, -) +from tests.unittests.helpers import mock, populate_dir, wrap_and_call MPATH = "cloudinit.sources.DataSourceVMware." PRODUCT_NAME_FILE_PATH = "/sys/class/dmi/id/product_name" @@ -169,85 +164,75 @@ def common_patches(): yield -class TestDataSourceVMware(CiTestCase): +class TestDataSourceVMware: """ Test common functionality that is not transport specific. """ - with_logs = True - - def setUp(self): - super(TestDataSourceVMware, self).setUp() - self.tmp = self.tmp_dir() - - def test_no_data_access_method(self): - ds = get_ds(self.tmp) + def test_no_data_access_method(self, DS): + ds = DS(settings.CFG_BUILTIN) with mock.patch( "cloudinit.sources.DataSourceVMware.is_vmware_platform", return_value=False, ): ret = ds.get_data() - self.assertFalse(ret) + assert not ret def test_convert_to_netifaces_ipv4_format(self): netifaces_format = DataSourceVMware.convert_to_netifaces_ipv4_format( VMW_IPV4_NETDEV_ADDR ) - self.assertEqual(netifaces_format, VMW_IPV4_NETIFACES_ADDR) + assert netifaces_format == VMW_IPV4_NETIFACES_ADDR def test_convert_to_netifaces_ipv6_format(self): netifaces_format = DataSourceVMware.convert_to_netifaces_ipv6_format( VMW_IPV6_NETDEV_ADDR ) - self.assertEqual(netifaces_format, VMW_IPV6_NETIFACES_ADDR) + assert netifaces_format == VMW_IPV6_NETIFACES_ADDR netifaces_format = DataSourceVMware.convert_to_netifaces_ipv6_format( VMW_IPV6_NETDEV_PEER_ADDR ) - self.assertEqual(netifaces_format, VMW_IPV6_NETIFACES_PEER_ADDR) + assert netifaces_format == VMW_IPV6_NETIFACES_PEER_ADDR @mock.patch("cloudinit.sources.DataSourceVMware.get_default_ip_addrs") def test_get_host_info_ipv4(self, m_fn_ipaddr): m_fn_ipaddr.return_value = ("10.10.10.1", None) host_info = DataSourceVMware.get_host_info() - self.assertTrue(host_info) - self.assertTrue(host_info["hostname"]) - self.assertTrue(host_info["hostname"] == "host.cloudinit.test") - self.assertTrue(host_info["local-hostname"]) - self.assertTrue(host_info["local_hostname"]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4] == "10.10.10.1") - self.assertFalse(host_info.get(DataSourceVMware.LOCAL_IPV6)) + assert host_info + assert host_info["hostname"] + assert host_info["hostname"] == "host.cloudinit.test" + assert host_info["local-hostname"] + assert host_info["local_hostname"] + assert host_info[DataSourceVMware.LOCAL_IPV4] + assert host_info[DataSourceVMware.LOCAL_IPV4] == "10.10.10.1" + assert not host_info.get(DataSourceVMware.LOCAL_IPV6) @mock.patch("cloudinit.sources.DataSourceVMware.get_default_ip_addrs") def test_get_host_info_ipv6(self, m_fn_ipaddr): m_fn_ipaddr.return_value = (None, "2001:db8::::::8888") host_info = DataSourceVMware.get_host_info() - self.assertTrue(host_info) - self.assertTrue(host_info["hostname"]) - self.assertTrue(host_info["hostname"] == "host.cloudinit.test") - self.assertTrue(host_info["local-hostname"]) - self.assertTrue(host_info["local_hostname"]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV6]) - self.assertTrue( - host_info[DataSourceVMware.LOCAL_IPV6] == "2001:db8::::::8888" - ) - self.assertFalse(host_info.get(DataSourceVMware.LOCAL_IPV4)) + assert host_info + assert host_info["hostname"] + assert host_info["hostname"] == "host.cloudinit.test" + assert host_info["local-hostname"] + assert host_info["local_hostname"] + assert host_info[DataSourceVMware.LOCAL_IPV6] + assert host_info[DataSourceVMware.LOCAL_IPV6] == "2001:db8::::::8888" + assert not host_info.get(DataSourceVMware.LOCAL_IPV4) @mock.patch("cloudinit.sources.DataSourceVMware.get_default_ip_addrs") def test_get_host_info_dual(self, m_fn_ipaddr): m_fn_ipaddr.return_value = ("10.10.10.1", "2001:db8::::::8888") host_info = DataSourceVMware.get_host_info() - self.assertTrue(host_info) - self.assertTrue(host_info["hostname"]) - self.assertTrue(host_info["hostname"] == "host.cloudinit.test") - self.assertTrue(host_info["local-hostname"]) - self.assertTrue(host_info["local_hostname"]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4] == "10.10.10.1") - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV6]) - self.assertTrue( - host_info[DataSourceVMware.LOCAL_IPV6] == "2001:db8::::::8888" - ) + assert host_info + assert host_info["hostname"] + assert host_info["hostname"] == "host.cloudinit.test" + assert host_info["local-hostname"] + assert host_info["local_hostname"] + assert host_info[DataSourceVMware.LOCAL_IPV4] + assert host_info[DataSourceVMware.LOCAL_IPV4] == "10.10.10.1" + assert host_info[DataSourceVMware.LOCAL_IPV6] + assert host_info[DataSourceVMware.LOCAL_IPV6] == "2001:db8::::::8888" # TODO migrate this entire test suite to pytest then parameterize @mock.patch("cloudinit.netinfo.route_info") @@ -266,8 +251,8 @@ def test_get_default_ip_addrs_ipv4only( ipv4=[VMW_IPV4_NETDEV_ADDR] ) ipv4, ipv6 = DataSourceVMware.get_default_ip_addrs() - self.assertEqual(ipv4, "10.85.130.116") - self.assertEqual(ipv6, None) + assert ipv4 == "10.85.130.116" + assert ipv6 is None @mock.patch("cloudinit.netinfo.route_info") @mock.patch("cloudinit.netinfo.netdev_info") @@ -284,8 +269,8 @@ def test_get_default_ip_addrs_ipv6only( ipv6=[VMW_IPV6_NETDEV_ADDR] ) ipv4, ipv6 = DataSourceVMware.get_default_ip_addrs() - self.assertEqual(ipv4, None) - self.assertEqual(ipv6, "fd42:baa2:3dd:17a:216:3eff:fe16:db54/64") + assert ipv4 is None + assert ipv6 == "fd42:baa2:3dd:17a:216:3eff:fe16:db54/64" @mock.patch("cloudinit.netinfo.route_info") @mock.patch("cloudinit.netinfo.netdev_info") @@ -303,8 +288,8 @@ def test_get_default_ip_addrs_dualstack( ipv6=[VMW_IPV6_NETDEV_ADDR], ) ipv4, ipv6 = DataSourceVMware.get_default_ip_addrs() - self.assertEqual(ipv4, "10.85.130.116") - self.assertEqual(ipv6, "fd42:baa2:3dd:17a:216:3eff:fe16:db54/64") + assert ipv4 == "10.85.130.116" + assert ipv6 == "fd42:baa2:3dd:17a:216:3eff:fe16:db54/64" @mock.patch("cloudinit.netinfo.route_info") @mock.patch("cloudinit.netinfo.netdev_info") @@ -336,8 +321,8 @@ def test_get_default_ip_addrs_multiaddr( ], ) ipv4, ipv6 = DataSourceVMware.get_default_ip_addrs() - self.assertEqual(ipv4, None) - self.assertEqual(ipv6, None) + assert ipv4 is None + assert ipv6 is None @mock.patch("cloudinit.netinfo.route_info") @mock.patch("cloudinit.netinfo.netdev_info") @@ -364,11 +349,11 @@ def test_get_default_ip_addrs_nodefault( ipv6=[VMW_IPV6_NETDEV_ADDR], ) ipv4, ipv6 = DataSourceVMware.get_default_ip_addrs() - self.assertEqual(ipv4, None) - self.assertEqual(ipv6, None) + assert ipv4 is None + assert ipv6 is None @mock.patch("cloudinit.sources.DataSourceVMware.get_host_info") - def test_wait_on_network(self, m_fn): + def test_wait_on_network(self, m_fn, caplog): metadata = { DataSourceVMware.WAIT_ON_NETWORK: { DataSourceVMware.WAIT_ON_NETWORK_IPV4: True, @@ -422,22 +407,31 @@ def test_wait_on_network(self, m_fn): host_info = DataSourceVMware.wait_on_network(metadata) - logs = self.logs.getvalue() expected_logs = [ - "DEBUG: waiting on network: wait4=True, " - "ready4=False, wait6=False, ready6=False\n", - "DEBUG: waiting on network complete\n", + ( + "cloudinit.sources.DataSourceVMware", + DEBUG, + ( + "waiting on network: wait4=True, " + "ready4=False, wait6=False, ready6=False" + ), + ), + ( + "cloudinit.sources.DataSourceVMware", + DEBUG, + "waiting on network complete", + ), ] for log in expected_logs: - self.assertIn(log, logs) + assert log in caplog.record_tuples - self.assertTrue(host_info) - self.assertTrue(host_info["hostname"]) - self.assertTrue(host_info["hostname"] == "host.cloudinit.test") - self.assertTrue(host_info["local-hostname"]) - self.assertTrue(host_info["local_hostname"]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4]) - self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4] == "10.10.10.1") + assert host_info + assert host_info["hostname"] + assert host_info["hostname"] == "host.cloudinit.test" + assert host_info["local-hostname"] + assert host_info["local_hostname"] + assert host_info[DataSourceVMware.LOCAL_IPV4] + assert host_info[DataSourceVMware.LOCAL_IPV4] == "10.10.10.1" @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_set_value") def test_advertise_update_events(self, m_set_fn): @@ -450,349 +444,324 @@ def test_advertise_update_events(self, m_set_fn): "rpctool", len, ) - self.assertEqual(2, m_set_fn.call_count) - self.assertEqual( - "network=boot;boot-new-instance;hotplug", supported_events - ) - self.assertEqual("network=boot-new-instance;hotplug", enabled_events) + assert 2 == m_set_fn.call_count + assert "network=boot;boot-new-instance;hotplug" == supported_events + assert "network=boot-new-instance;hotplug" == enabled_events - def test_extra_hotplug_udev_rules(self): - ds = get_ds(self.tmp) - self.assertEqual( - VMW_EXPECTED_EXTRA_HOTPLUG_UDEV_RULES, ds.extra_hotplug_udev_rules + def test_extra_hotplug_udev_rules(self, DS): + ds = DS(settings.CFG_BUILTIN) + assert ( + VMW_EXPECTED_EXTRA_HOTPLUG_UDEV_RULES + == ds.extra_hotplug_udev_rules ) -class TestDataSourceVMwareEnvVars(FilesystemMockingTestCase): +class TestDataSourceVMwareEnvVars: """ Test the envvar transport. """ - def setUp(self): - super(TestDataSourceVMwareEnvVars, self).setUp() - self.tmp = self.tmp_dir() - os.environ[DataSourceVMware.VMX_GUESTINFO] = "1" - self.create_system_files() - - def tearDown(self): - del os.environ[DataSourceVMware.VMX_GUESTINFO] - return super().tearDown() - - def create_system_files(self): - rootd = self.tmp_dir() + @pytest.fixture(autouse=True) + def env_and_files(self, fake_filesystem, monkeypatch, tmpdir): + monkeypatch.setenv(DataSourceVMware.VMX_GUESTINFO, "1") populate_dir( - rootd, - { - DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, - }, + str(tmpdir), + {DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID}, ) - self.assertTrue(self.reRoot(rootd)) - def assert_get_data_ok(self, m_fn, m_fn_call_count=6): - ds = get_ds(self.tmp) + def assert_get_data_ok(self, DS, m_fn, m_fn_call_count=6): + ds = DS(settings.CFG_BUILTIN) ret = ds.get_data() - self.assertTrue(ret) - self.assertEqual(m_fn_call_count, m_fn.call_count) - self.assertEqual( - ds.data_access_method, DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR + assert ret + assert m_fn_call_count == m_fn.call_count + assert ( + ds.data_access_method == DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR ) return ds - def assert_metadata(self, metadata, m_fn, m_fn_call_count=6): - ds = self.assert_get_data_ok(m_fn, m_fn_call_count) - assert_metadata(self, ds, metadata) + def assert_metadata(self, DS, metadata, m_fn, m_fn_call_count=6): + ds = self.assert_get_data_ok(DS, m_fn, m_fn_call_count) + assert_metadata(ds, metadata) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_subplatform(self, m_fn): + def test_get_subplatform(self, m_fn, DS): m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] - ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4) - self.assertEqual( - ds.subplatform, - "%s (%s)" - % ( - DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR, - DataSourceVMware.get_guestinfo_envvar_key_name("metadata"), - ), + ds = self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) + assert ds.subplatform == "%s (%s)" % ( + DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR, + DataSourceVMware.get_guestinfo_envvar_key_name("metadata"), ) # Test to ensure that network is configured from metadata on each boot. - self.assertSetEqual( - DataSourceVMware.DEFAULT_UPDATE_EVENTS[EventScope.NETWORK], - ds.default_update_events[EventScope.NETWORK], + assert ( + DataSourceVMware.DEFAULT_UPDATE_EVENTS[EventScope.NETWORK] + == ds.default_update_events[EventScope.NETWORK] ) - self.assertSetEqual( - DataSourceVMware.SUPPORTED_UPDATE_EVENTS[EventScope.NETWORK], - ds.supported_update_events[EventScope.NETWORK], + assert ( + DataSourceVMware.SUPPORTED_UPDATE_EVENTS[EventScope.NETWORK] + == ds.supported_update_events[EventScope.NETWORK] ) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_metadata_only(self, m_fn): + def test_get_data_metadata_only(self, m_fn, DS): m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_userdata_only(self, m_fn): + def test_get_data_userdata_only(self, m_fn, DS): m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_vendordata_only(self, m_fn): + def test_get_data_vendordata_only(self, m_fn, DS): m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_metadata_base64(self, m_fn): + def test_get_data_metadata_base64(self, m_fn, DS): data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) m_fn.side_effect = [data, "base64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_metadata_b64(self, m_fn): + def test_get_data_metadata_b64(self, m_fn, DS): data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) m_fn.side_effect = [data, "b64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_metadata_gzip_base64(self, m_fn): + def test_get_data_metadata_gzip_base64(self, m_fn, DS): data = VMW_METADATA_YAML.encode("utf-8") data = gzip.compress(data) data = base64.b64encode(data) m_fn.side_effect = [data, "gzip+base64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_get_data_metadata_gz_b64(self, m_fn): + def test_get_data_metadata_gz_b64(self, m_fn, DS): data = VMW_METADATA_YAML.encode("utf-8") data = gzip.compress(data) data = base64.b64encode(data) m_fn.side_effect = [data, "gz+b64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_metadata_single_ssh_key(self, m_fn): + def test_metadata_single_ssh_key(self, m_fn, DS): metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) metadata["public_keys"] = VMW_SINGLE_KEY metadata_yaml = safeyaml.dumps(metadata) m_fn.side_effect = [metadata_yaml, "", "", ""] - self.assert_metadata(metadata, m_fn, m_fn_call_count=4) + self.assert_metadata(DS, metadata, m_fn, m_fn_call_count=4) @mock.patch( "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ) - def test_metadata_multiple_ssh_keys(self, m_fn): + def test_metadata_multiple_ssh_keys(self, m_fn, DS): metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) metadata["public_keys"] = VMW_MULTIPLE_KEYS metadata_yaml = safeyaml.dumps(metadata) m_fn.side_effect = [metadata_yaml, "", "", ""] - self.assert_metadata(metadata, m_fn, m_fn_call_count=4) + self.assert_metadata(DS, metadata, m_fn, m_fn_call_count=4) -class TestDataSourceVMwareGuestInfo(FilesystemMockingTestCase): +class TestDataSourceVMwareGuestInfo: """ Test the guestinfo transport on a VMware platform. """ - def setUp(self): - super(TestDataSourceVMwareGuestInfo, self).setUp() - self.tmp = self.tmp_dir() - self.create_system_files() - - def create_system_files(self): - rootd = self.tmp_dir() + @pytest.fixture(autouse=True) + def create_files(self, fake_filesystem, tmpdir): populate_dir( - rootd, + str(tmpdir), { DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, PRODUCT_NAME_FILE_PATH: PRODUCT_NAME, }, ) - self.assertTrue(self.reRoot(rootd)) - def assert_get_data_ok(self, m_fn, m_fn_call_count=6): - ds = get_ds(self.tmp) + def assert_get_data_ok(self, DS, m_fn, m_fn_call_count=6): + ds = DS(settings.CFG_BUILTIN) ret = ds.get_data() - self.assertTrue(ret) - self.assertEqual(m_fn_call_count, m_fn.call_count) - self.assertEqual( - ds.data_access_method, - DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, + assert ret + assert m_fn_call_count == m_fn.call_count + assert ( + ds.data_access_method + == DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO ) return ds - def assert_metadata(self, metadata, m_fn, m_fn_call_count=6): - ds = self.assert_get_data_ok(m_fn, m_fn_call_count) - assert_metadata(self, ds, metadata) + def assert_metadata(self, DS, metadata, m_fn, m_fn_call_count=6): + ds = self.assert_get_data_ok(DS, m_fn, m_fn_call_count) + assert_metadata(ds, metadata) def test_ds_valid_on_vmware_platform(self): system_type = dmi.read_dmi_data("system-product-name") - self.assertEqual(system_type, PRODUCT_NAME) + assert system_type == PRODUCT_NAME @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_subplatform(self, m_which_fn, m_fn): + def test_get_subplatform(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] - ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4) - self.assertEqual( - ds.subplatform, - "%s (%s)" - % ( - DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, - DataSourceVMware.get_guestinfo_key_name("metadata"), - ), + ds = self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) + assert ds.subplatform == "%s (%s)" % ( + DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, + DataSourceVMware.get_guestinfo_key_name("metadata"), ) # Test to ensure that network is configured from metadata on each boot. - self.assertSetEqual( - DataSourceVMware.DEFAULT_UPDATE_EVENTS[EventScope.NETWORK], - ds.default_update_events[EventScope.NETWORK], + assert ( + DataSourceVMware.DEFAULT_UPDATE_EVENTS[EventScope.NETWORK] + == ds.default_update_events[EventScope.NETWORK] ) - self.assertSetEqual( - DataSourceVMware.SUPPORTED_UPDATE_EVENTS[EventScope.NETWORK], - ds.supported_update_events[EventScope.NETWORK], + assert ( + DataSourceVMware.SUPPORTED_UPDATE_EVENTS[EventScope.NETWORK] + == ds.supported_update_events[EventScope.NETWORK] ) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_metadata_with_vmware_rpctool(self, m_which_fn, m_fn): + def test_get_data_metadata_with_vmware_rpctool( + self, m_which_fn, m_fn, DS, tmpdir + ): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_fn.side_effect = [VMW_METADATA_YAML, "", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.exec_vmware_rpctool") @mock.patch("cloudinit.sources.DataSourceVMware.which") def test_get_data_metadata_non_zero_exit_code_fallback_to_vmtoolsd( - self, m_which_fn, m_exec_vmware_rpctool_fn, m_fn + self, m_which_fn, m_exec_vmware_rpctool_fn, m_fn, DS, tmpdir ): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_exec_vmware_rpctool_fn.side_effect = ProcessExecutionError( exit_code=1 ) m_fn.side_effect = [VMW_METADATA_YAML, "", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.exec_vmware_rpctool") @mock.patch("cloudinit.sources.DataSourceVMware.which") def test_get_data_metadata_vmware_rpctool_not_found_fallback_to_vmtoolsd( - self, m_which_fn, m_exec_vmware_rpctool_fn, m_fn + self, m_which_fn, m_exec_vmware_rpctool_fn, m_fn, DS, tmpdir ): m_which_fn.side_effect = ["vmtoolsd", None] m_fn.side_effect = [VMW_METADATA_YAML, "", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_userdata_only(self, m_which_fn, m_fn): + def test_get_data_userdata_only(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_vendordata_only(self, m_which_fn, m_fn): + def test_get_data_vendordata_only(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_metadata_single_ssh_key(self, m_which_fn, m_fn): + def test_metadata_single_ssh_key(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) metadata["public_keys"] = VMW_SINGLE_KEY metadata_yaml = safeyaml.dumps(metadata) m_fn.side_effect = [metadata_yaml, "", "", ""] - self.assert_metadata(metadata, m_fn, m_fn_call_count=4) + self.assert_metadata(DS, metadata, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_metadata_multiple_ssh_keys(self, m_which_fn, m_fn): + def test_metadata_multiple_ssh_keys(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) metadata["public_keys"] = VMW_MULTIPLE_KEYS metadata_yaml = safeyaml.dumps(metadata) m_fn.side_effect = [metadata_yaml, "", "", ""] - self.assert_metadata(metadata, m_fn, m_fn_call_count=4) + self.assert_metadata(DS, metadata, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_metadata_base64(self, m_which_fn, m_fn): + def test_get_data_metadata_base64(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) m_fn.side_effect = [data, "base64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_metadata_b64(self, m_which_fn, m_fn): + def test_get_data_metadata_b64(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) m_fn.side_effect = [data, "b64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_metadata_gzip_base64(self, m_which_fn, m_fn): + def test_get_data_metadata_gzip_base64(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] data = VMW_METADATA_YAML.encode("utf-8") data = gzip.compress(data) data = base64.b64encode(data) m_fn.side_effect = [data, "gzip+base64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_get_data_metadata_gz_b64(self, m_which_fn, m_fn): + def test_get_data_metadata_gz_b64(self, m_which_fn, m_fn, DS): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] data = VMW_METADATA_YAML.encode("utf-8") data = gzip.compress(data) data = base64.b64encode(data) m_fn.side_effect = [data, "gz+b64", "", ""] - self.assert_get_data_ok(m_fn, m_fn_call_count=4) + self.assert_get_data_ok(DS, m_fn, m_fn_call_count=4) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_set_value") @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") - def test_advertise_update_events(self, m_which_fn, m_get_fn, m_set_fn): + def test_advertise_update_events( + self, m_which_fn, m_get_fn, m_set_fn, DS, tmpdir + ): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_get_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] - ds = self.assert_get_data_ok(m_get_fn, m_fn_call_count=4) + ds = self.assert_get_data_ok(DS, m_get_fn, m_fn_call_count=4) supported_events, enabled_events = ds.advertise_update_events({}) - self.assertEqual(2, m_set_fn.call_count) - self.assertEqual( - "network=boot;boot-new-instance;hotplug", supported_events - ) - self.assertEqual("network=boot-new-instance;hotplug", enabled_events) + assert 2 == m_set_fn.call_count + assert "network=boot;boot-new-instance;hotplug" == supported_events + assert "network=boot-new-instance;hotplug" == enabled_events @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_set_value") @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") def test_advertise_update_events_with_events_from_user_data( - self, m_which_fn, m_get_fn, m_set_fn + self, m_which_fn, m_get_fn, m_set_fn, DS, tmpdir ): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_get_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] - ds = self.assert_get_data_ok(m_get_fn, m_fn_call_count=4) + ds = self.assert_get_data_ok(DS, m_get_fn, m_fn_call_count=4) supported_events, enabled_events = ds.advertise_update_events( { "updates": { @@ -802,18 +771,14 @@ def test_advertise_update_events_with_events_from_user_data( }, } ) - self.assertEqual(2, m_set_fn.call_count) - self.assertEqual( - "network=boot;boot-new-instance;hotplug", supported_events - ) - self.assertEqual("network=boot", enabled_events) + assert 2 == m_set_fn.call_count + assert "network=boot;boot-new-instance;hotplug" == supported_events + assert "network=boot" == enabled_events @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") @mock.patch("cloudinit.sources.DataSourceVMware.which") def test_extra_hotplug_udev_rules_with_net_drivers( - self, - m_which_fn, - m_get_fn, + self, m_which_fn, m_get_fn, DS, tmpdir ): m_which_fn.side_effect = ["vmtoolsd", "vmware-rpctool"] m_get_fn.side_effect = [ @@ -824,67 +789,47 @@ def test_extra_hotplug_udev_rules_with_net_drivers( "", "", ] - ds = self.assert_get_data_ok(m_get_fn, m_fn_call_count=4) + ds = self.assert_get_data_ok(DS, m_get_fn, m_fn_call_count=4) ds.init_extra_hotplug_udev_rules() - self.assertEqual( - VMW_EXPECTED_EXTRA_HOTPLUG_UDEV_RULES_VMXNET, - ds.extra_hotplug_udev_rules, + assert ( + VMW_EXPECTED_EXTRA_HOTPLUG_UDEV_RULES_VMXNET + == ds.extra_hotplug_udev_rules ) -class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase): +class TestDataSourceVMwareGuestInfo_InvalidPlatform: """ Test the guestinfo transport on a non-VMware platform. """ - def setUp(self): - super(TestDataSourceVMwareGuestInfo_InvalidPlatform, self).setUp() - self.tmp = self.tmp_dir() - self.create_system_files() - - def create_system_files(self): - rootd = self.tmp_dir() + @pytest.fixture(autouse=True) + def create_files(self, fake_filesystem, tmpdir): populate_dir( - rootd, - { - DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, - }, + str(tmpdir), + {DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID}, ) - self.assertTrue(self.reRoot(rootd)) @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") - def test_ds_invalid_on_non_vmware_platform(self, m_fn): + def test_ds_invalid_on_non_vmware_platform(self, m_fn, DS): system_type = dmi.read_dmi_data("system-product-name") - self.assertEqual(system_type, None) + assert system_type is None m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] - ds = get_ds(self.tmp) + ds = DS(settings.CFG_BUILTIN) ret = ds.get_data() - self.assertFalse(ret) + assert not ret -class TestDataSourceVMwareIMC(CiTestCase): +class TestDataSourceVMwareIMC: """ Test the VMware Guest OS Customization transport """ - with_logs = True - - def setUp(self): - super(TestDataSourceVMwareIMC, self).setUp() - self.datasource = DataSourceVMware.DataSourceVMware - self.tdir = self.tmp_dir() - - def test_get_subplatform(self): - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": True}, - distro={}, - paths=paths, - ) + def test_get_subplatform(self, DS, tmpdir): + ds = DS({"disable_vmware_customization": True}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -893,7 +838,7 @@ def test_get_subplatform(self): ) util.write_file(conf_file, conf_content) # Prepare the meta data file - metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_file = os.path.join(tmpdir, "test-meta") metadata_content = dedent( """\ { @@ -924,37 +869,32 @@ def test_get_subplatform(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds._get_data, ) - self.assertTrue(result) - - self.assertEqual( - ds.subplatform, - "%s (%s)" - % ( - DataSourceVMware.DATA_ACCESS_METHOD_IMC, - DataSourceVMware.get_imc_key_name("metadata"), - ), + assert result + + assert ds.subplatform == "%s (%s)" % ( + DataSourceVMware.DATA_ACCESS_METHOD_IMC, + DataSourceVMware.get_imc_key_name("metadata"), ) # Test to ensure that network is configured from metadata on each boot. - self.assertSetEqual( - DataSourceVMware.DEFAULT_UPDATE_EVENTS[EventScope.NETWORK], - ds.default_update_events[EventScope.NETWORK], + assert ( + DataSourceVMware.DEFAULT_UPDATE_EVENTS[EventScope.NETWORK] + == ds.default_update_events[EventScope.NETWORK] ) - self.assertSetEqual( - DataSourceVMware.SUPPORTED_UPDATE_EVENTS[EventScope.NETWORK], - ds.supported_update_events[EventScope.NETWORK], + assert ( + DataSourceVMware.SUPPORTED_UPDATE_EVENTS[EventScope.NETWORK] + == ds.supported_update_events[EventScope.NETWORK] ) - def test_get_data_false_on_none_dmi_data(self): + def test_get_data_false_on_none_dmi_data(self, caplog, DS): """When dmi for system-product-name is None, get_data returns False.""" - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource(sys_cfg={}, distro={}, paths=paths) + ds = DS({}) result = wrap_and_call( "cloudinit.sources.DataSourceVMware", { @@ -962,24 +902,23 @@ def test_get_data_false_on_none_dmi_data(self): }, ds.get_data, ) - self.assertFalse(result, "Expected False return from ds.get_data") - self.assertIn("No system-product-name found", self.logs.getvalue()) + assert not result, "Expected False return from ds.get_data" + assert "No system-product-name found" in caplog.text - def test_get_imc_data_vmware_customization_disabled(self): + def test_get_imc_data_vmware_customization_disabled( + self, caplog, DS, tmpdir + ): """ When vmware customization is disabled via sys_cfg and allow_raw_data is disabled via ds_cfg, log a message. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={ + ds = DS( + { "disable_vmware_customization": True, "datasource": {"VMware": {"allow_raw_data": False}}, }, - distro={}, - paths=paths, ) - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [MISC] @@ -994,27 +933,23 @@ def test_get_imc_data_vmware_customization_disabled(self): }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - self.assertIn( - "Customization for VMware platform is disabled", - self.logs.getvalue(), - ) + assert result == (None, None, None) + assert "Customization for VMware platform is disabled" in caplog.text - def test_get_imc_data_vmware_customization_sys_cfg_disabled(self): + def test_get_imc_data_vmware_customization_sys_cfg_disabled( + self, caplog, DS, tmpdir + ): """ When vmware customization is disabled via sys_cfg and no meta data is found, log a message. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={ + ds = DS( + { "disable_vmware_customization": True, "datasource": {"VMware": {"allow_raw_data": True}}, }, - distro={}, - paths=paths, ) - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [MISC] @@ -1027,34 +962,30 @@ def test_get_imc_data_vmware_customization_sys_cfg_disabled(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - self.assertIn( - "No allowed customization configuration data found", - self.logs.getvalue(), + assert result == (None, None, None) + assert ( + "No allowed customization configuration data found" in caplog.text ) - def test_get_imc_data_allow_raw_data_disabled(self): + def test_get_imc_data_allow_raw_data_disabled(self, caplog, DS, tmpdir): """ When allow_raw_data is disabled via ds_cfg and meta data is found, log a message. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={ + ds = DS( + { "disable_vmware_customization": False, "datasource": {"VMware": {"allow_raw_data": False}}, }, - distro={}, - paths=paths, ) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1067,29 +998,26 @@ def test_get_imc_data_allow_raw_data_disabled(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - self.assertIn( - "No allowed customization configuration data found", - self.logs.getvalue(), + assert result == (None, None, None) + assert ( + "No allowed customization configuration data found" in caplog.text ) - def test_get_imc_data_vmware_customization_enabled(self): + @pytest.mark.allow_subp_for("vmware-rpctool") + def test_get_imc_data_vmware_customization_enabled( + self, caplog, DS, tmpdir + ): """ When cloud-init workflow for vmware is enabled via sys_cfg log a message. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": False}, - distro={}, - paths=paths, - ) - conf_file = self.tmp_path("test-cust", self.tdir) + ds = DS({"disable_vmware_customization": False}) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CUSTOM-SCRIPT] @@ -1108,31 +1036,23 @@ def test_get_imc_data_vmware_customization_enabled(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - custom_script = self.tmp_path("test-script", self.tdir) - self.assertIn( - "Script %s not found!!" % custom_script, - self.logs.getvalue(), - ) + assert result == (None, None, None) + custom_script = os.path.join(tmpdir, "test-script") + assert "Script %s not found!!" % custom_script in caplog.text - def test_get_imc_data_cust_script_disabled(self): + def test_get_imc_data_cust_script_disabled(self, caplog, DS, tmpdir): """ If custom script is disabled by VMware tools configuration, log a message. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": False}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": False}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CUSTOM-SCRIPT] @@ -1143,7 +1063,7 @@ def test_get_imc_data_cust_script_disabled(self): ) util.write_file(conf_file, conf_content) # Prepare the custom sript - customscript = self.tmp_path("test-script", self.tdir) + customscript = os.path.join(tmpdir, "test-script") util.write_file(customscript, "This is the post cust script") with mock.patch( @@ -1159,30 +1079,22 @@ def test_get_imc_data_cust_script_disabled(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - self.assertIn( - "Custom script is disabled by VM Administrator", - self.logs.getvalue(), - ) + assert result == (None, None, None) + assert "Custom script is disabled by VM Administrator" in caplog.text - def test_get_imc_data_cust_script_enabled(self): + def test_get_imc_data_cust_script_enabled(self, caplog, DS, tmpdir): """ If custom script is enabled by VMware tools configuration, execute the script. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": False}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": False}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CUSTOM-SCRIPT] @@ -1208,32 +1120,26 @@ def test_get_imc_data_cust_script_enabled(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) + assert result == (None, None, None) # Verify custom script is trying to be executed - custom_script = self.tmp_path("test-script", self.tdir) - self.assertIn( - "Script %s not found!!" % custom_script, - self.logs.getvalue(), - ) + custom_script = os.path.join(tmpdir, "test-script") + assert "Script %s not found!!" % custom_script in caplog.text - def test_get_imc_data_force_run_post_script_is_yes(self): + def test_get_imc_data_force_run_post_script_is_yes( + self, caplog, DS, tmpdir + ): """ If DEFAULT-RUN-POST-CUST-SCRIPT is yes, custom script could run if enable-custom-scripts is not defined in VM Tools configuration """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": False}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": False}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") # set DEFAULT-RUN-POST-CUST-SCRIPT = yes so that enable-custom-scripts # default value is TRUE conf_content = dedent( @@ -1265,33 +1171,25 @@ def my_get_tools_config(*args, **kwargs): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) + assert result == (None, None, None) # Verify custom script still runs although it is # disabled by VMware Tools - custom_script = self.tmp_path("test-script", self.tdir) - self.assertIn( - "Script %s not found!!" % custom_script, - self.logs.getvalue(), - ) + custom_script = os.path.join(tmpdir, "test-script") + assert "Script %s not found!!" % custom_script in caplog.text - def test_get_data_cloudinit_metadata_json(self): + def test_get_data_cloudinit_metadata_json(self, DS, tmpdir): """ Test metadata can be loaded to cloud-init metadata and network. The metadata format is json. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": True}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": True}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1300,7 +1198,7 @@ def test_get_data_cloudinit_metadata_json(self): ) util.write_file(conf_file, conf_content) # Prepare the meta data file - metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_file = os.path.join(tmpdir, "test-meta") metadata_content = dedent( """\ { @@ -1331,31 +1229,26 @@ def test_get_data_cloudinit_metadata_json(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds._get_data, ) - self.assertTrue(result) - self.assertEqual("cloud-vm", ds.metadata["instance-id"]) - self.assertEqual("my-host.domain.com", ds.metadata["local-hostname"]) - self.assertEqual(2, ds.network_config["version"]) - self.assertTrue(ds.network_config["ethernets"]["eths"]["dhcp4"]) + assert result + assert "cloud-vm" == ds.metadata["instance-id"] + assert "my-host.domain.com" == ds.metadata["local-hostname"] + assert 2 == ds.network_config["version"] + assert ds.network_config["ethernets"]["eths"]["dhcp4"] - def test_get_data_cloudinit_metadata_yaml(self): + def test_get_data_cloudinit_metadata_yaml(self, DS, tmpdir): """ Test metadata can be loaded to cloud-init metadata and network. The metadata format is yaml. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": True}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": True}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1364,7 +1257,7 @@ def test_get_data_cloudinit_metadata_yaml(self): ) util.write_file(conf_file, conf_content) # Prepare the meta data file - metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_file = os.path.join(tmpdir, "test-meta") metadata_content = dedent( """\ instance-id: cloud-vm @@ -1389,31 +1282,28 @@ def test_get_data_cloudinit_metadata_yaml(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds._get_data, ) - self.assertTrue(result) - self.assertEqual("cloud-vm", ds.metadata["instance-id"]) - self.assertEqual("my-host.domain.com", ds.metadata["local-hostname"]) - self.assertEqual(2, ds.network_config["version"]) - self.assertTrue(ds.network_config["ethernets"]["nics"]["dhcp4"]) - - def test_get_imc_data_cloudinit_metadata_not_valid(self): + assert result + assert "cloud-vm" == ds.metadata["instance-id"] + assert "my-host.domain.com" == ds.metadata["local-hostname"] + assert 2 == ds.network_config["version"] + assert ds.network_config["ethernets"]["nics"]["dhcp4"] + + def test_get_imc_data_cloudinit_metadata_not_valid( + self, caplog, DS, tmpdir + ): """ Test metadata is not JSON or YAML format, log a message """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": True}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": True}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1423,7 +1313,7 @@ def test_get_imc_data_cloudinit_metadata_not_valid(self): util.write_file(conf_file, conf_content) # Prepare the meta data file - metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_file = os.path.join(tmpdir, "test-meta") metadata_content = "[This is not json or yaml format]a=b" util.write_file(metadata_file, metadata_content) @@ -1436,30 +1326,26 @@ def test_get_imc_data_cloudinit_metadata_not_valid(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds.get_data, ) - self.assertFalse(result) - self.assertIn( - "expected '', but found ''", - self.logs.getvalue(), + assert not result + assert ( + "expected '', but found ''" in caplog.text ) - def test_get_imc_data_cloudinit_metadata_not_found(self): + def test_get_imc_data_cloudinit_metadata_not_found( + self, caplog, DS, tmpdir + ): """ Test metadata file can't be found, log a message """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": True}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": True}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1478,28 +1364,23 @@ def test_get_imc_data_cloudinit_metadata_not_found(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - self.assertIn("Meta data file is not found", self.logs.getvalue()) + assert result == (None, None, None) + assert "Meta data file is not found" in caplog.text - def test_get_data_cloudinit_userdata(self): + def test_get_data_cloudinit_userdata(self, DS, tmpdir): """ Test user data can be loaded to cloud-init user data. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": False}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": False}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1510,7 +1391,7 @@ def test_get_data_cloudinit_userdata(self): util.write_file(conf_file, conf_content) # Prepare the meta data file - metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_file = os.path.join(tmpdir, "test-meta") metadata_content = dedent( """\ instance-id: cloud-vm @@ -1527,7 +1408,7 @@ def test_get_data_cloudinit_userdata(self): util.write_file(metadata_file, metadata_content) # Prepare the user data file - userdata_file = self.tmp_path("test-user", self.tdir) + userdata_file = os.path.join(tmpdir, "test-user") userdata_content = "This is the user data" util.write_file(userdata_file, userdata_content) @@ -1540,29 +1421,26 @@ def test_get_data_cloudinit_userdata(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds._get_data, ) - self.assertTrue(result) - self.assertEqual("cloud-vm", ds.metadata["instance-id"]) - self.assertEqual(userdata_content, ds.userdata_raw) + assert result + assert "cloud-vm" == ds.metadata["instance-id"] + assert userdata_content == ds.userdata_raw - def test_get_imc_data_cloudinit_userdata_not_found(self): + def test_get_imc_data_cloudinit_userdata_not_found( + self, caplog, DS, tmpdir + ): """ Test userdata file can't be found. """ - paths = helpers.Paths({"cloud_dir": self.tdir}) - ds = self.datasource( - sys_cfg={"disable_vmware_customization": True}, - distro={}, - paths=paths, - ) + ds = DS({"disable_vmware_customization": True}) # Prepare the conf file - conf_file = self.tmp_path("test-cust", self.tdir) + conf_file = os.path.join(tmpdir, "test-cust") conf_content = dedent( """\ [CLOUDINIT] @@ -1573,7 +1451,7 @@ def test_get_imc_data_cloudinit_userdata_not_found(self): util.write_file(conf_file, conf_content) # Prepare the meta data file - metadata_file = self.tmp_path("test-meta", self.tdir) + metadata_file = os.path.join(tmpdir, "test-meta") metadata_content = dedent( """\ instance-id: cloud-vm @@ -1600,61 +1478,54 @@ def test_get_imc_data_cloudinit_userdata_not_found(self): { "dmi.read_dmi_data": "vmware", "util.del_dir": True, - "guestcust_util.search_file": self.tdir, + "guestcust_util.search_file": tmpdir, "guestcust_util.wait_for_cust_cfg_file": conf_file, - "guestcust_util.get_imc_dir_path": self.tdir, + "guestcust_util.get_imc_dir_path": tmpdir, }, ds.get_imc_data_fn, ) - self.assertEqual(result, (None, None, None)) - self.assertIn("Userdata file is not found", self.logs.getvalue()) + assert result == (None, None, None) + assert "Userdata file is not found" in caplog.text -class TestDataSourceVMwareIMC_MarkerFiles(CiTestCase): - def setUp(self): - super(TestDataSourceVMwareIMC_MarkerFiles, self).setUp() - self.tdir = self.tmp_dir() +class TestDataSourceVMwareIMC_MarkerFiles: - def test_false_when_markerid_none(self): + def test_false_when_markerid_none(self, tmpdir): """Return False when markerid provided is None.""" - self.assertFalse( - guestcust_util.check_marker_exists( - markerid=None, marker_dir=self.tdir - ) + assert not guestcust_util.check_marker_exists( + markerid=None, marker_dir=tmpdir ) - def test_markerid_file_exist(self): + def test_markerid_file_exist(self, tmpdir): """Return False when markerid file path does not exist, True otherwise.""" - self.assertFalse(guestcust_util.check_marker_exists("123", self.tdir)) - marker_file = self.tmp_path(".markerfile-123.txt", self.tdir) + assert not guestcust_util.check_marker_exists("123", tmpdir) + marker_file = os.path.join(tmpdir, ".markerfile-123.txt") util.write_file(marker_file, "") - self.assertTrue(guestcust_util.check_marker_exists("123", self.tdir)) + assert guestcust_util.check_marker_exists("123", tmpdir) - def test_marker_file_setup(self): + def test_marker_file_setup(self, tmpdir): """Test creation of marker files.""" - markerfilepath = self.tmp_path(".markerfile-hi.txt", self.tdir) - self.assertFalse(os.path.exists(markerfilepath)) - guestcust_util.setup_marker_files(marker_id="hi", marker_dir=self.tdir) - self.assertTrue(os.path.exists(markerfilepath)) + markerfilepath = os.path.join(tmpdir, ".markerfile-hi.txt") + assert not os.path.exists(markerfilepath) + guestcust_util.setup_marker_files(marker_id="hi", marker_dir=tmpdir) + assert os.path.exists(markerfilepath) -def assert_metadata(test_obj, ds, metadata): - test_obj.assertEqual(metadata.get("instance-id"), ds.get_instance_id()) - test_obj.assertEqual( - metadata.get("local-hostname"), ds.get_hostname().hostname - ) +def assert_metadata(ds, metadata): + assert metadata.get("instance-id") == ds.get_instance_id() + assert metadata.get("local-hostname") == ds.get_hostname().hostname expected_public_keys = metadata.get("public_keys") if not isinstance(expected_public_keys, list): expected_public_keys = [expected_public_keys] - test_obj.assertEqual(expected_public_keys, ds.get_public_ssh_keys()) - test_obj.assertIsInstance(ds.get_public_ssh_keys(), list) + assert expected_public_keys == ds.get_public_ssh_keys() + assert isinstance(ds.get_public_ssh_keys(), list) -def get_ds(temp_dir): - ds = DataSourceVMware.DataSourceVMware( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": temp_dir}) +@pytest.fixture +def DS(paths): + return lambda sys_cfg: DataSourceVMware.DataSourceVMware( + sys_cfg, {}, paths ) - return ds From 4e3ca8208994ac8a66a5506d6e162ad46a3e41cb Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 7 Jul 2025 17:24:23 +0200 Subject: [PATCH 012/150] test: Consolidate use of paths fixture (#6289) Move it to tests/unittests/conftets.py and substitute MockPaths with it. --- conftest.py | 18 +--------- tests/unittests/conftest.py | 34 ++++++++----------- .../distros/package_management/test_apt.py | 5 +-- tests/unittests/sources/test_maas.py | 4 +-- tests/unittests/test_helpers.py | 14 ++++---- 5 files changed, 27 insertions(+), 48 deletions(-) diff --git a/conftest.py b/conftest.py index 36320f237dc..5d7aa5638d1 100644 --- a/conftest.py +++ b/conftest.py @@ -19,7 +19,7 @@ import pytest -from cloudinit import helpers, subp, util +from cloudinit import subp, util @pytest.fixture(autouse=True, scope="function") @@ -197,22 +197,6 @@ def mocked_responses(): yield rsps -@pytest.fixture -def paths(tmpdir): - """ - Return a helpers.Paths object configured to use a tmpdir. - - (This uses the builtin tmpdir fixture.) - """ - dirs = { - "cloud_dir": tmpdir.mkdir("cloud_dir").strpath, - "docs_dir": tmpdir.mkdir("docs_dir").strpath, - "run_dir": tmpdir.mkdir("run_dir").strpath, - "templates_dir": tmpdir.mkdir("templates_dir").strpath, - } - return helpers.Paths(dirs) - - @pytest.fixture(autouse=True, scope="session") def monkeypatch_system_info(): def my_system_info(): diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index deb765691e5..5d725cd56aa 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -7,9 +7,7 @@ import pytest -from cloudinit import atomic_helper -from cloudinit import helpers as ch -from cloudinit import lifecycle +from cloudinit import atomic_helper, helpers, lifecycle from cloudinit import user_data as ud from cloudinit import util from cloudinit.gpg import GPG @@ -177,25 +175,21 @@ def tmp_path(tmpdir): @pytest.fixture -def MockPaths(tmp_path): - class _MockPaths(ch.Paths): - def __init__(self, path_cfgs: dict, ds=None): - super().__init__(path_cfgs=path_cfgs, ds=ds) +def paths(tmpdir) -> helpers.Paths: + """ + Return a helpers.Paths object configured to use a tmpdir. - self.cloud_dir: str = path_cfgs.get( - "cloud_dir", f"{tmp_path}/var/lib/cloud" - ) - self.run_dir: str = path_cfgs.get( - "run_dir", f"{tmp_path}/run/cloud/" - ) - self.template_dir: str = path_cfgs.get( - "templates_dir", f"{tmp_path}/etc/cloud/templates/" - ) - - return _MockPaths + (This uses the builtin tmpdir fixture.) + """ + dirs = { + "cloud_dir": tmpdir.mkdir("cloud_dir").strpath, + "docs_dir": tmpdir.mkdir("docs_dir").strpath, + "run_dir": tmpdir.mkdir("run_dir").strpath, + "templates_dir": tmpdir.mkdir("templates_dir").strpath, + } + return helpers.Paths(dirs) @pytest.fixture -def ud_proc(MockPaths): - paths = MockPaths({}) +def ud_proc(paths): return ud.UserDataProcessor(paths) diff --git a/tests/unittests/distros/package_management/test_apt.py b/tests/unittests/distros/package_management/test_apt.py index 8f333be3d38..8eefa07a057 100644 --- a/tests/unittests/distros/package_management/test_apt.py +++ b/tests/unittests/distros/package_management/test_apt.py @@ -119,13 +119,14 @@ def test_search_stem(self, m_subp, m_which, mocker): @pytest.fixture(scope="function") -def apt_paths(MockPaths, tmpdir): +def apt_paths(paths, tmpdir): with mock.patch.object( apt, "APT_LOCK_FILES", [f"{tmpdir}/{FILE}" for FILE in apt.APT_LOCK_FILES], ): - yield MockPaths({}, FakeDataSource()) + paths.datasource = FakeDataSource() + yield paths class TestUpdatePackageSources: diff --git a/tests/unittests/sources/test_maas.py b/tests/unittests/sources/test_maas.py index 9f0dbb34884..980307f5d17 100644 --- a/tests/unittests/sources/test_maas.py +++ b/tests/unittests/sources/test_maas.py @@ -224,7 +224,7 @@ def tests_wb_local_stage_detects_datasource_on_initramfs_network( assert expected == ds.get_data() @responses.activate - def test_get_data_with_retry(self, mocker, MockPaths, caplog): + def test_get_data_with_retry(self, mocker, paths, caplog): """Ensure we can get data from IMDS even if some attempts fail.""" mocker.patch("time.sleep") metadata_url = "http://169.254.169.254/MAAS/metadata" @@ -260,7 +260,7 @@ def test_get_data_with_retry(self, mocker, MockPaths, caplog): ) cfg = {"datasource": {"MAAS": {"metadata_url": metadata_url}}} - ds = DataSourceMAAS.DataSourceMAAS(cfg, MockDistro(), MockPaths({})) + ds = DataSourceMAAS.DataSourceMAAS(cfg, MockDistro(), paths) assert ds.get_data() assert ds.metadata["instance-id"] == "i-123" assert ds.metadata["local-hostname"] == "myhostname" diff --git a/tests/unittests/test_helpers.py b/tests/unittests/test_helpers.py index cd81745067d..ec3abe197d8 100644 --- a/tests/unittests/test_helpers.py +++ b/tests/unittests/test_helpers.py @@ -17,23 +17,23 @@ def get_instance_id(self): class TestPaths: - def test_get_ipath_and_instance_id_with_slashes(self, MockPaths): + def test_get_ipath_and_instance_id_with_slashes(self, paths): myds = MyDataSource(sys_cfg={}, distro=None, paths={}) myds._instance_id = "/foo/bar" + paths.datasource = myds safe_iid = "_foo_bar" - mypaths = MockPaths({}, myds) assert ( - os.path.join(mypaths.cloud_dir, "instances", safe_iid) - == mypaths.get_ipath() + os.path.join(paths.cloud_dir, "instances", safe_iid) + == paths.get_ipath() ) - def test_get_ipath_and_empty_instance_id_returns_none(self, MockPaths): + def test_get_ipath_and_empty_instance_id_returns_none(self, paths): myds = MyDataSource(sys_cfg={}, distro=None, paths={}) myds._instance_id = None - mypaths = MockPaths({}, myds) + paths.datasource = myds - assert mypaths.get_ipath() is None + assert paths.get_ipath() is None class Testcloud_init_project_dir: From a5083f8104fce6e8835d997019ed5fc5c7aa0433 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 7 Jul 2025 17:35:39 +0200 Subject: [PATCH 013/150] test: drop ResponsesTestCase (#6277) --- tests/unittests/helpers.py | 84 +++++---- tests/unittests/net/test_dhcp.py | 5 +- tests/unittests/sources/helpers/test_ec2.py | 143 +++++++------- tests/unittests/sources/test_aliyun.py | 10 +- tests/unittests/sources/test_exoscale.py | 197 +++++++++----------- tests/unittests/sources/test_gce.py | 22 ++- tests/unittests/sources/test_openstack.py | 57 ++++-- tests/unittests/sources/test_scaleway.py | 126 +++++++------ 8 files changed, 339 insertions(+), 305 deletions(-) diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index e443b8b8c16..c44ec79c3f6 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -25,6 +25,7 @@ SchemaValidationError, validate_cloudconfig_schema, ) +from cloudinit.helpers import Paths from cloudinit.templater import JINJA_AVAILABLE from tests.helpers import cloud_init_project_dir from tests.hypothesis_jsonschema import HAS_HYPOTHESIS_JSONSCHEMA @@ -391,49 +392,54 @@ def reRooted(self, root=None): self.patched_funcs.close() -class CiRequestsMock(responses.RequestsMock): - def assert_call_count(self, url: str, count: int) -> bool: - """Focal and older have a version of responses which does - not carry this attribute. This can be removed when focal - is no longer supported. - """ - if hasattr(super(), "_ensure_url_default_path"): - return super().assert_call_count(url, count) - - def _ensure_url_default_path(url): - if isinstance(url, str): - url_parts = list(urlsplit(url)) - if url_parts[2] == "": - url_parts[2] = "/" - url = urlunsplit(url_parts) - return url - - call_count = len( - [ - 1 - for call in self.calls - if call.request.url == _ensure_url_default_path(url) - ] +def responses_assert_call_count(url: str, count: int) -> bool: + """Focal and older have a version of responses which does + not carry this attribute. This can be removed when focal + is no longer supported. + """ + if hasattr(responses, "assert_call_count"): + return responses.assert_call_count(url, count) + + def _ensure_url_default_path(url): + if isinstance(url, str): + url_parts = list(urlsplit(url)) + if url_parts[2] == "": + url_parts[2] = "/" + url = urlunsplit(url_parts) + return url + + call_count = len( + [ + 1 + for call in responses.calls + if call.request.url == _ensure_url_default_path(url) + ] + ) + if call_count == count: + return True + else: + raise AssertionError( + f"Expected URL '{url}' to be called {count} times. " + f"Called {call_count} times." ) - if call_count == count: - return True - else: - raise AssertionError( - f"Expected URL '{url}' to be called {count} times. " - f"Called {call_count} times." - ) -class ResponsesTestCase(CiTestCase): - def setUp(self): - super().setUp() - self.responses = CiRequestsMock(assert_all_requests_are_fired=False) - self.responses.start() +def get_mock_paths(temp_dir): + class MockPaths(Paths): + def __init__(self, path_cfgs: dict, ds=None): + super().__init__(path_cfgs=path_cfgs, ds=ds) - def tearDown(self): - self.responses.stop() - self.responses.reset() - super().tearDown() + self.cloud_dir: str = path_cfgs.get( + "cloud_dir", f"{temp_dir}/var/lib/cloud" + ) + self.run_dir: str = path_cfgs.get( + "run_dir", f"{temp_dir}/run/cloud/" + ) + self.template_dir: str = path_cfgs.get( + "templates_dir", f"{temp_dir}/etc/cloud/templates/" + ) + + return MockPaths class SchemaTestCaseMixin(unittest.TestCase): diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py index ae2d08041f5..708566422d9 100644 --- a/tests/unittests/net/test_dhcp.py +++ b/tests/unittests/net/test_dhcp.py @@ -7,6 +7,7 @@ from textwrap import dedent import pytest +import responses from cloudinit.distros import alpine, amazon, centos, debian, freebsd, rhel from cloudinit.distros.ubuntu import Distro @@ -27,7 +28,6 @@ from cloudinit.util import ensure_file, load_binary_file, subp, write_file from tests.unittests.helpers import ( CiTestCase, - ResponsesTestCase, example_netdev, mock, populate_dir, @@ -814,9 +814,10 @@ def test_multiple_files(self): ) +@responses.activate @pytest.mark.usefixtures("disable_netdev_info") @mock.patch("cloudinit.net.ephemeral._check_connectivity_to_imds") -class TestEphemeralDhcpNoNetworkSetup(ResponsesTestCase): +class TestEphemeralDhcpNoNetworkSetup(CiTestCase): @mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") def test_ephemeral_dhcp_no_network_if_url_connectivity( self, m_dhcp, m_imds diff --git a/tests/unittests/sources/helpers/test_ec2.py b/tests/unittests/sources/helpers/test_ec2.py index aa250893a2a..2f4cdf06e68 100644 --- a/tests/unittests/sources/helpers/test_ec2.py +++ b/tests/unittests/sources/helpers/test_ec2.py @@ -4,163 +4,170 @@ from cloudinit import url_helper as uh from cloudinit.sources.helpers import ec2 -from tests.unittests import helpers -class TestEc2Util(helpers.ResponsesTestCase): +class TestEc2Util: VERSION = "latest" + @responses.activate def test_userdata_fetch(self): - self.responses.add( + responses.add( responses.GET, "http://169.254.169.254/%s/user-data" % (self.VERSION), body="stuff", status=200, ) userdata = ec2.get_instance_userdata(self.VERSION) - self.assertEqual("stuff", userdata.decode("utf-8")) + assert "stuff" == userdata.decode("utf-8") + @responses.activate def test_userdata_fetch_fail_not_found(self): - self.responses.add( + responses.add( responses.GET, "http://169.254.169.254/%s/user-data" % (self.VERSION), status=404, ) userdata = ec2.get_instance_userdata(self.VERSION, retries=0) - self.assertEqual(b"", userdata) + assert b"" == userdata + @responses.activate def test_userdata_fetch_fail_server_dead(self): - self.responses.add( + responses.add( responses.GET, "http://169.254.169.254/%s/user-data" % (self.VERSION), status=500, ) userdata = ec2.get_instance_userdata(self.VERSION, retries=0) - self.assertEqual(b"", userdata) + assert b"" == userdata + @responses.activate def test_userdata_fetch_fail_server_not_found(self): - self.responses.add( + responses.add( responses.GET, "http://169.254.169.254/%s/user-data" % (self.VERSION), status=404, ) userdata = ec2.get_instance_userdata(self.VERSION) - self.assertEqual(b"", userdata) + assert b"" == userdata + @responses.activate def test_metadata_fetch_no_keys(self): base_url = "http://169.254.169.254/%s/meta-data/" % (self.VERSION) - self.responses.add( + responses.add( responses.GET, base_url, status=200, body="\n".join(["hostname", "instance-id", "ami-launch-index"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "hostname"), status=200, body="ec2.fake.host.name.com", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "instance-id"), status=200, body="123", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "ami-launch-index"), status=200, body="1", ) md = ec2.get_instance_metadata(self.VERSION, retries=0) - self.assertEqual(md["hostname"], "ec2.fake.host.name.com") - self.assertEqual(md["instance-id"], "123") - self.assertEqual(md["ami-launch-index"], "1") + assert md["hostname"] == "ec2.fake.host.name.com" + assert md["instance-id"] == "123" + assert md["ami-launch-index"] == "1" + @responses.activate def test_metadata_fetch_key(self): base_url = "http://169.254.169.254/%s/meta-data/" % (self.VERSION) - self.responses.add( + responses.add( responses.GET, base_url, status=200, body="\n".join(["hostname", "instance-id", "public-keys/"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "hostname"), status=200, body="ec2.fake.host.name.com", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "instance-id"), status=200, body="123", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "public-keys/"), status=200, body="0=my-public-key", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "public-keys/0/openssh-key"), status=200, body="ssh-rsa AAAA.....wZEf my-public-key", ) md = ec2.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) - self.assertEqual(md["hostname"], "ec2.fake.host.name.com") - self.assertEqual(md["instance-id"], "123") - self.assertEqual(1, len(md["public-keys"])) + assert md["hostname"] == "ec2.fake.host.name.com" + assert md["instance-id"] == "123" + assert 1 == len(md["public-keys"]) + @responses.activate def test_metadata_fetch_with_2_keys(self): base_url = "http://169.254.169.254/%s/meta-data/" % (self.VERSION) - self.responses.add( + responses.add( responses.GET, base_url, status=200, body="\n".join(["hostname", "instance-id", "public-keys/"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "hostname"), status=200, body="ec2.fake.host.name.com", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "instance-id"), status=200, body="123", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "public-keys/"), status=200, body="\n".join(["0=my-public-key", "1=my-other-key"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "public-keys/0/openssh-key"), status=200, body="ssh-rsa AAAA.....wZEf my-public-key", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "public-keys/1/openssh-key"), status=200, body="ssh-rsa AAAA.....wZEf my-other-key", ) md = ec2.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) - self.assertEqual(md["hostname"], "ec2.fake.host.name.com") - self.assertEqual(md["instance-id"], "123") - self.assertEqual(2, len(md["public-keys"])) + assert md["hostname"] == "ec2.fake.host.name.com" + assert md["instance-id"] == "123" + assert 2 == len(md["public-keys"]) + @responses.activate def test_metadata_fetch_bdm(self): base_url = "http://169.254.169.254/%s/meta-data/" % (self.VERSION) - self.responses.add( + responses.add( responses.GET, base_url, status=200, @@ -168,89 +175,90 @@ def test_metadata_fetch_bdm(self): ["hostname", "instance-id", "block-device-mapping/"] ), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "hostname"), status=200, body="ec2.fake.host.name.com", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "instance-id"), status=200, body="123", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "block-device-mapping/"), status=200, body="\n".join(["ami", "ephemeral0"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "block-device-mapping/ami"), status=200, body="sdb", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "block-device-mapping/ephemeral0"), status=200, body="sdc", ) md = ec2.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) - self.assertEqual(md["hostname"], "ec2.fake.host.name.com") - self.assertEqual(md["instance-id"], "123") + assert md["hostname"] == "ec2.fake.host.name.com" + assert md["instance-id"] == "123" bdm = md["block-device-mapping"] - self.assertEqual(2, len(bdm)) - self.assertEqual(bdm["ami"], "sdb") - self.assertEqual(bdm["ephemeral0"], "sdc") + assert 2 == len(bdm) + assert bdm["ami"] == "sdb" + assert bdm["ephemeral0"] == "sdc" + @responses.activate def test_metadata_no_security_credentials(self): base_url = "http://169.254.169.254/%s/meta-data/" % (self.VERSION) - self.responses.add( + responses.add( responses.GET, base_url, status=200, body="\n".join(["instance-id", "iam/"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "instance-id"), status=200, body="i-0123451689abcdef0", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "iam/"), status=200, body="\n".join(["info/", "security-credentials/"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "iam/info/"), status=200, body="LastUpdated", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "iam/info/LastUpdated"), status=200, body="2016-10-27T17:29:39Z", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "iam/security-credentials/"), status=200, body="ReadOnly/", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "iam/security-credentials/ReadOnly/"), status=200, body="\n".join(["LastUpdated", "Expiration"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url( base_url, "iam/security-credentials/ReadOnly/LastUpdated" @@ -258,7 +266,7 @@ def test_metadata_no_security_credentials(self): status=200, body="2016-10-27T17:28:17Z", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url( base_url, "iam/security-credentials/ReadOnly/Expiration" @@ -267,12 +275,13 @@ def test_metadata_no_security_credentials(self): body="2016-10-28T00:00:34Z", ) md = ec2.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) - self.assertEqual(md["instance-id"], "i-0123451689abcdef0") + assert md["instance-id"] == "i-0123451689abcdef0" iam = md["iam"] - self.assertEqual(1, len(iam)) - self.assertEqual(iam["info"]["LastUpdated"], "2016-10-27T17:29:39Z") - self.assertNotIn("security-credentials", iam) + assert 1 == len(iam) + assert iam["info"]["LastUpdated"] == "2016-10-27T17:29:39Z" + assert "security-credentials" not in iam + @responses.activate def test_metadata_children_with_invalid_character(self): def _skip_tags(exception): if isinstance(exception, uh.UrlError) and exception.code == 404: @@ -282,30 +291,30 @@ def _skip_tags(exception): return False base_url = "http://169.254.169.254/%s/meta-data/" % (self.VERSION) - self.responses.add( + responses.add( responses.GET, base_url, status=200, body="\n".join(["tags/", "ami-launch-index"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "tags/"), status=200, body="\n".join(["test/invalid", "valid"]), ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "tags/valid"), status=200, body="OK", ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "tags/test/invalid"), status=404, ) - self.responses.add( + responses.add( responses.GET, uh.combine_url(base_url, "ami-launch-index"), status=200, @@ -317,8 +326,8 @@ def _skip_tags(exception): timeout=0.1, retrieval_exception_ignore_cb=_skip_tags, ) - self.assertEqual(md["tags"]["valid"], "OK") - self.assertEqual(md["tags"]["test/invalid"], "(skipped)") - self.assertEqual(md["ami-launch-index"], "1") + assert md["tags"]["valid"] == "OK" + assert md["tags"]["test/invalid"] == "(skipped)" + assert md["ami-launch-index"] == "1" md = ec2.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) - self.assertEqual(len(md), 0) + assert len(md) == 0 diff --git a/tests/unittests/sources/test_aliyun.py b/tests/unittests/sources/test_aliyun.py index 2d61ff8af93..ca635019414 100644 --- a/tests/unittests/sources/test_aliyun.py +++ b/tests/unittests/sources/test_aliyun.py @@ -97,7 +97,7 @@ - echo hello world > /tmp/vendor""" -class TestAliYunDatasource(test_helpers.ResponsesTestCase): +class TestAliYunDatasource(test_helpers.CiTestCase): def setUp(self): super(TestAliYunDatasource, self).setUp() cfg = {"datasource": {"AliYun": {"timeout": "1", "max_wait": "1"}}} @@ -198,7 +198,7 @@ def register_helper(register, base_url, body): register_helper(register, url, v) register(base_url, "\n".join(vals) + "\n") - register = functools.partial(self.responses.add, responses.GET) + register = functools.partial(responses.add, responses.GET) register_helper(register, base_url, data) def regist_default_server(self, register_json_meta_path=True): @@ -213,7 +213,7 @@ def regist_default_server(self, register_json_meta_path=True): ) self.register_mock_metaserver(self.identity_url, self.default_identity) - self.responses.add(responses.PUT, self.token_url, "API-TOKEN") + responses.add(responses.PUT, self.token_url, "API-TOKEN") def _test_get_data(self): self.assertEqual(self.ds.metadata, self.default_metadata) @@ -238,6 +238,7 @@ def _test_host_name(self): self.default_metadata["hostname"], self.ds.get_hostname().hostname ) + @responses.activate @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") def test_with_mock_server(self, m_is_aliyun, m_resolv): @@ -256,6 +257,7 @@ def test_with_mock_server(self, m_is_aliyun, m_resolv): "metadata (http://100.100.100.200)", self.ds.subplatform ) + @responses.activate @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") def test_with_mock_server_without_json_path(self, m_is_aliyun, m_resolv): @@ -274,6 +276,7 @@ def test_with_mock_server_without_json_path(self, m_is_aliyun, m_resolv): "metadata (http://100.100.100.200)", self.ds.subplatform ) + @responses.activate @mock.patch("cloudinit.net.ephemeral.EphemeralIPv6Network") @mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network") @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") @@ -320,6 +323,7 @@ def test_aliyun_local_with_mock_server( "metadata (http://100.100.100.200)", self.ds.subplatform ) + @responses.activate @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") def test_returns_false_when_not_on_aliyun(self, m_is_aliyun): """If is_aliyun returns false, then get_data should return False.""" diff --git a/tests/unittests/sources/test_exoscale.py b/tests/unittests/sources/test_exoscale.py index 82b567d74ed..08cbeeb913d 100644 --- a/tests/unittests/sources/test_exoscale.py +++ b/tests/unittests/sources/test_exoscale.py @@ -16,7 +16,7 @@ get_password, read_metadata, ) -from tests.unittests.helpers import ResponsesTestCase, mock +from tests.unittests.helpers import mock TEST_PASSWORD_URL = "{}:{}/{}/".format( METADATA_URL, PASSWORD_SERVER_PORT, API_VERSION @@ -27,176 +27,157 @@ TEST_USERDATA_URL = "{}/{}/user-data".format(METADATA_URL, API_VERSION) -class TestDatasourceExoscale(ResponsesTestCase): - def setUp(self): - super(TestDatasourceExoscale, self).setUp() - self.tmp = self.tmp_dir() - self.password_url = TEST_PASSWORD_URL - self.metadata_url = TEST_METADATA_URL - self.userdata_url = TEST_USERDATA_URL - +class TestDatasourceExoscale: + @responses.activate def test_password_saved(self): """The password is not set when it is not found in the metadata service.""" - self.responses.add( - responses.GET, self.password_url, body="saved_password" - ) - self.assertFalse(get_password()) + responses.add(responses.GET, TEST_PASSWORD_URL, body="saved_password") + assert not get_password() + @responses.activate def test_password_empty(self): """No password is set if the metadata service returns an empty string.""" - self.responses.add(responses.GET, self.password_url, body="") - self.assertFalse(get_password()) + responses.add(responses.GET, TEST_PASSWORD_URL, body="") + assert not get_password() + @responses.activate def test_password(self): """The password is set to what is found in the metadata service.""" expected_password = "p@ssw0rd" - self.responses.add( - responses.GET, self.password_url, body=expected_password - ) + responses.add(responses.GET, TEST_PASSWORD_URL, body=expected_password) password = get_password() - self.assertEqual(expected_password, password) + assert expected_password == password - def test_activate_removes_set_passwords_semaphore(self): + def test_activate_removes_set_passwords_semaphore(self, tmp_path): """Allow set_passwords to run every boot by removing the semaphore.""" - path = helpers.Paths({"cloud_dir": self.tmp}) - sem_dir = self.tmp_path("instance/sem", dir=self.tmp) + path = helpers.Paths({"cloud_dir": str(tmp_path)}) + sem_dir = str(tmp_path / "instance/sem") util.ensure_dir(sem_dir) sem_file = os.path.join(sem_dir, "config_set_passwords") with open(sem_file, "w") as stream: stream.write("") ds = DataSourceExoscale({}, None, path) ds.activate(None, None) - self.assertFalse(os.path.exists(sem_file)) + assert not os.path.exists(sem_file) - def test_get_data(self): + @responses.activate + def test_get_data(self, tmp_path): """The datasource conforms to expected behavior when supplied full test data.""" - path = helpers.Paths({"run_dir": self.tmp}) + path = helpers.Paths({"run_dir": str(tmp_path)}) ds = DataSourceExoscale({}, None, path) ds.ds_detect = lambda: True expected_password = "p@ssw0rd" expected_id = "12345" expected_hostname = "myname" expected_userdata = "#cloud-config" - self.responses.add( - responses.GET, self.userdata_url, body=expected_userdata - ) - self.responses.add( - responses.GET, self.password_url, body=expected_password - ) - self.responses.add( + responses.add(responses.GET, TEST_USERDATA_URL, body=expected_userdata) + responses.add(responses.GET, TEST_PASSWORD_URL, body=expected_password) + responses.add( responses.GET, - self.metadata_url, + TEST_METADATA_URL, body="instance-id\nlocal-hostname", ) - self.responses.add( + responses.add( responses.GET, - "{}local-hostname".format(self.metadata_url), + "{}local-hostname".format(TEST_METADATA_URL), body=expected_hostname, ) - self.responses.add( + responses.add( responses.GET, - "{}instance-id".format(self.metadata_url), + "{}instance-id".format(TEST_METADATA_URL), body=expected_id, ) - self.assertTrue(ds._check_and_get_data()) - self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") - self.assertEqual( - ds.metadata, - {"instance-id": expected_id, "local-hostname": expected_hostname}, - ) - self.assertEqual( - ds.get_config_obj(), - { - "ssh_pwauth": True, - "password": expected_password, - "chpasswd": { - "expire": False, - }, + assert ds._check_and_get_data() + assert ds.userdata_raw.decode("utf-8") == "#cloud-config" + assert ds.metadata == { + "instance-id": expected_id, + "local-hostname": expected_hostname, + } + assert ds.get_config_obj() == { + "ssh_pwauth": True, + "password": expected_password, + "chpasswd": { + "expire": False, }, - ) + } - def test_get_data_saved_password(self): + @responses.activate + def test_get_data_saved_password(self, tmp_path): """The datasource conforms to expected behavior when saved_password is returned by the password server.""" - path = helpers.Paths({"run_dir": self.tmp}) + path = helpers.Paths({"run_dir": str(tmp_path)}) ds = DataSourceExoscale({}, None, path) ds.ds_detect = lambda: True expected_answer = "saved_password" expected_id = "12345" expected_hostname = "myname" expected_userdata = "#cloud-config" - self.responses.add( - responses.GET, self.userdata_url, body=expected_userdata - ) - self.responses.add( - responses.GET, self.password_url, body=expected_answer - ) - self.responses.add( + responses.add(responses.GET, TEST_USERDATA_URL, body=expected_userdata) + responses.add(responses.GET, TEST_PASSWORD_URL, body=expected_answer) + responses.add( responses.GET, - self.metadata_url, + TEST_METADATA_URL, body="instance-id\nlocal-hostname", ) - self.responses.add( + responses.add( responses.GET, - "{}local-hostname".format(self.metadata_url), + "{}local-hostname".format(TEST_METADATA_URL), body=expected_hostname, ) - self.responses.add( + responses.add( responses.GET, - "{}instance-id".format(self.metadata_url), + "{}instance-id".format(TEST_METADATA_URL), body=expected_id, ) - self.assertTrue(ds._check_and_get_data()) - self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") - self.assertEqual( - ds.metadata, - {"instance-id": expected_id, "local-hostname": expected_hostname}, - ) - self.assertEqual(ds.get_config_obj(), {}) + assert ds._check_and_get_data() + assert ds.userdata_raw.decode("utf-8") == "#cloud-config" + assert ds.metadata == { + "instance-id": expected_id, + "local-hostname": expected_hostname, + } + assert ds.get_config_obj() == {} - def test_get_data_no_password(self): + @responses.activate + def test_get_data_no_password(self, tmp_path): """The datasource conforms to expected behavior when no password is returned by the password server.""" - path = helpers.Paths({"run_dir": self.tmp}) + path = helpers.Paths({"run_dir": str(tmp_path)}) ds = DataSourceExoscale({}, None, path) ds.ds_detect = lambda: True expected_answer = "" expected_id = "12345" expected_hostname = "myname" expected_userdata = "#cloud-config" - self.responses.add( - responses.GET, self.userdata_url, body=expected_userdata - ) - self.responses.add( - responses.GET, self.password_url, body=expected_answer - ) - self.responses.add( + responses.add(responses.GET, TEST_USERDATA_URL, body=expected_userdata) + responses.add(responses.GET, TEST_PASSWORD_URL, body=expected_answer) + responses.add( responses.GET, - self.metadata_url, + TEST_METADATA_URL, body="instance-id\nlocal-hostname", ) - self.responses.add( + responses.add( responses.GET, - "{}local-hostname".format(self.metadata_url), + "{}local-hostname".format(TEST_METADATA_URL), body=expected_hostname, ) - self.responses.add( + responses.add( responses.GET, - "{}instance-id".format(self.metadata_url), + "{}instance-id".format(TEST_METADATA_URL), body=expected_id, ) - self.assertTrue(ds._check_and_get_data()) - self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config") - self.assertEqual( - ds.metadata, - {"instance-id": expected_id, "local-hostname": expected_hostname}, - ) - self.assertEqual(ds.get_config_obj(), {}) + assert ds._check_and_get_data() + assert ds.userdata_raw.decode("utf-8") == "#cloud-config" + assert ds.metadata == { + "instance-id": expected_id, + "local-hostname": expected_hostname, + } + assert ds.get_config_obj() == {} + @responses.activate @mock.patch("cloudinit.sources.DataSourceExoscale.get_password") def test_read_metadata_when_password_server_unreachable(self, m_password): """The read_metadata function returns partial results in case the @@ -206,35 +187,31 @@ def test_read_metadata_when_password_server_unreachable(self, m_password): expected_userdata = "#cloud-config" m_password.side_effect = requests.Timeout("Fake Connection Timeout") - self.responses.add( - responses.GET, self.userdata_url, body=expected_userdata - ) - self.responses.add( + responses.add(responses.GET, TEST_USERDATA_URL, body=expected_userdata) + responses.add( responses.GET, - self.metadata_url, + TEST_METADATA_URL, body="instance-id\nlocal-hostname", ) - self.responses.add( + responses.add( responses.GET, - "{}local-hostname".format(self.metadata_url), + "{}local-hostname".format(TEST_METADATA_URL), body=expected_hostname, ) - self.responses.add( + responses.add( responses.GET, - "{}instance-id".format(self.metadata_url), + "{}instance-id".format(TEST_METADATA_URL), body=expected_id, ) result = read_metadata() - self.assertIsNone(result.get("password")) - self.assertEqual( - result.get("user-data").decode("utf-8"), expected_userdata - ) + assert result.get("password") is None + assert result.get("user-data").decode("utf-8") == expected_userdata - def test_non_viable_platform(self): + def test_non_viable_platform(self, tmp_path): """The datasource fails fast when the platform is not viable.""" - path = helpers.Paths({"run_dir": self.tmp}) + path = helpers.Paths({"run_dir": str(tmp_path)}) ds = DataSourceExoscale({}, None, path) ds.ds_detect = lambda: False - self.assertFalse(ds._check_and_get_data()) + assert not ds._check_and_get_data() diff --git a/tests/unittests/sources/test_gce.py b/tests/unittests/sources/test_gce.py index 350ebd127a1..41bc52dbcfb 100644 --- a/tests/unittests/sources/test_gce.py +++ b/tests/unittests/sources/test_gce.py @@ -61,7 +61,7 @@ ) -class TestDataSourceGCE(test_helpers.ResponsesTestCase): +class TestDataSourceGCE(test_helpers.CiTestCase): with_logs = True def _make_distro(self, dtype, def_user=None): @@ -120,17 +120,19 @@ def _request_callback(request): else: return (404, request.headers, "") - self.responses.add_callback( + responses.add_callback( responses.GET, MD_URL_RE, callback=_request_callback, ) + @responses.activate def test_connection(self): self._set_mock_metadata(check_headers=HEADERS) success = self.ds.get_data() self.assertTrue(success) + @responses.activate def test_metadata(self): # UnicodeDecodeError if set to ds.userdata instead of userdata_raw meta = GCE_META.copy() @@ -151,6 +153,7 @@ def test_metadata(self): self.ds.get_userdata_raw(), ) + @responses.activate def test_metadata_partial(self): """test partial metadata (missing user-data in particular)""" self._set_mock_metadata(GCE_META_PARTIAL) @@ -163,6 +166,7 @@ def test_metadata_partial(self): shostname = GCE_META_PARTIAL.get("instance/hostname").split(".")[0] self.assertEqual(shostname, self.ds.get_hostname().hostname) + @responses.activate def test_userdata_no_encoding(self): """check that user-data is read.""" self._set_mock_metadata(GCE_USER_DATA_TEXT) @@ -172,6 +176,7 @@ def test_userdata_no_encoding(self): self.ds.get_userdata_raw(), ) + @responses.activate def test_metadata_encoding(self): """user-data is base64 encoded if user-data-encoding is 'base64'.""" self._set_mock_metadata(GCE_META_ENCODING) @@ -181,6 +186,7 @@ def test_metadata_encoding(self): decoded = b64decode(instance_data.get("user-data")) self.assertEqual(decoded, self.ds.get_userdata_raw()) + @responses.activate def test_missing_required_keys_return_false(self): for required_key in [ "instance/id", @@ -191,13 +197,15 @@ def test_missing_required_keys_return_false(self): del meta[required_key] self._set_mock_metadata(meta) self.assertEqual(False, self.ds.get_data()) - self.responses.reset() + responses.reset() + @responses.activate def test_no_ssh_keys_metadata(self): self._set_mock_metadata() self.ds.get_data() self.assertEqual([], self.ds.get_public_ssh_keys()) + @responses.activate def test_cloudinit_ssh_keys(self): valid_key = "ssh-rsa VALID {0}" invalid_key = "ssh-rsa INVALID {0}" @@ -235,6 +243,7 @@ def test_cloudinit_ssh_keys(self): expected = [valid_key.format(key) for key in range(3)] self.assertEqual(set(expected), set(self.ds.get_public_ssh_keys())) + @responses.activate @mock.patch(M_PATH + "ug_util") def test_default_user_ssh_keys(self, mock_ug_util): mock_ug_util.normalize_users_groups.return_value = None, None @@ -281,6 +290,7 @@ def test_default_user_ssh_keys(self, mock_ug_util): expected = [valid_key.format(key) for key in range(3)] self.assertEqual(set(expected), set(ubuntu_ds.get_public_ssh_keys())) + @responses.activate def test_instance_ssh_keys_override(self): valid_key = "ssh-rsa VALID {0}" invalid_key = "ssh-rsa INVALID {0}" @@ -304,6 +314,7 @@ def test_instance_ssh_keys_override(self): expected = [valid_key.format(key) for key in range(2)] self.assertEqual(set(expected), set(self.ds.get_public_ssh_keys())) + @responses.activate def test_block_project_ssh_keys_override(self): valid_key = "ssh-rsa VALID {0}" invalid_key = "ssh-rsa INVALID {0}" @@ -326,6 +337,7 @@ def test_block_project_ssh_keys_override(self): expected = [valid_key.format(0)] self.assertEqual(set(expected), set(self.ds.get_public_ssh_keys())) + @responses.activate def test_only_last_part_of_zone_used_for_availability_zone(self): self._set_mock_metadata() r = self.ds.get_data() @@ -401,6 +413,7 @@ def test_publish_host_keys(self, m_readurl): self.ds.publish_host_keys(hostkeys) m_readurl.assert_has_calls(readurl_expected_calls, any_order=True) + @responses.activate @mock.patch( M_PATH + "EphemeralDHCPv4", autospec=True, @@ -418,6 +431,7 @@ def test_local_datasource_uses_ephemeral_dhcp( ds._get_data() assert m_dhcp.call_count == 1 + @responses.activate @mock.patch(M_PATH + "read_md") @mock.patch( M_PATH + "EphemeralDHCPv4", @@ -479,6 +493,7 @@ def test_local_datasource_tries_on_multi_nic( self.logs.getvalue(), ) + @responses.activate @mock.patch( M_PATH + "EphemeralDHCPv4", autospec=True, @@ -489,6 +504,7 @@ def test_datasource_doesnt_use_ephemeral_dhcp(self, m_dhcp): ds._get_data() assert m_dhcp.call_count == 0 + @responses.activate @mock.patch( M_PATH + "EphemeralDHCPv4", autospec=True, diff --git a/tests/unittests/sources/test_openstack.py b/tests/unittests/sources/test_openstack.py index 4df3273ce5c..2d2e8a71157 100644 --- a/tests/unittests/sources/test_openstack.py +++ b/tests/unittests/sources/test_openstack.py @@ -145,7 +145,7 @@ def _read_metadata_service(): return ds.read_metadata_service(BASE_URL, retries=0, timeout=0.1) -class TestOpenStackDataSource(test_helpers.ResponsesTestCase): +class TestOpenStackDataSource(test_helpers.CiTestCase): with_logs = True VERSION = "latest" @@ -154,13 +154,14 @@ def setUp(self): super(TestOpenStackDataSource, self).setUp() self.tmp = self.tmp_dir() + @responses.activate def test_successful(self): _register_uris( self.VERSION, EC2_FILES, EC2_META, OS_FILES, - responses_mock=self.responses, + responses_mock=responses, ) f = _read_metadata_service() self.assertEqual(VENDOR_DATA, f.get("vendordata")) @@ -185,9 +186,10 @@ def test_successful(self): "b0fa911b-69d4-4476-bbe2-1c92bff6535c", metadata.get("instance-id") ) + @responses.activate def test_no_ec2(self): _register_uris( - self.VERSION, {}, {}, OS_FILES, responses_mock=self.responses + self.VERSION, {}, {}, OS_FILES, responses_mock=responses ) f = _read_metadata_service() self.assertEqual(VENDOR_DATA, f.get("vendordata")) @@ -198,16 +200,18 @@ def test_no_ec2(self): self.assertEqual({}, f.get("ec2-metadata")) self.assertEqual(2, f.get("version")) + @responses.activate def test_bad_metadata(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("meta_data.json"): os_files.pop(k, None) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) self.assertRaises(openstack.NonReadable, _read_metadata_service) + @responses.activate def test_bad_uuid(self): os_files = copy.deepcopy(OS_FILES) os_meta = copy.deepcopy(OSTACK_META) @@ -216,17 +220,18 @@ def test_bad_uuid(self): if k.endswith("meta_data.json"): os_files[k] = json.dumps(os_meta) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) self.assertRaises(BrokenMetadata, _read_metadata_service) + @responses.activate def test_userdata_empty(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("user_data"): os_files.pop(k, None) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) f = _read_metadata_service() self.assertEqual(VENDOR_DATA, f.get("vendordata")) @@ -235,62 +240,68 @@ def test_userdata_empty(self): self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) self.assertFalse(f.get("userdata")) + @responses.activate def test_vendordata_empty(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("vendor_data.json"): os_files.pop(k, None) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) f = _read_metadata_service() self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) self.assertFalse(f.get("vendordata")) + @responses.activate def test_vendordata2_empty(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("vendor_data2.json"): os_files.pop(k, None) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) f = _read_metadata_service() self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) self.assertFalse(f.get("vendordata2")) + @responses.activate def test_vendordata_invalid(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("vendor_data.json"): os_files[k] = "{" # some invalid json _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) self.assertRaises(BrokenMetadata, _read_metadata_service) + @responses.activate def test_vendordata2_invalid(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("vendor_data2.json"): os_files[k] = "{" # some invalid json _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) self.assertRaises(BrokenMetadata, _read_metadata_service) + @responses.activate def test_metadata_invalid(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("meta_data.json"): os_files[k] = "{" # some invalid json _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) self.assertRaises(BrokenMetadata, _read_metadata_service) + @responses.activate @test_helpers.mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") def test_datasource(self, m_dhcp): _register_uris( @@ -298,7 +309,7 @@ def test_datasource(self, m_dhcp): EC2_FILES, EC2_META, OS_FILES, - responses_mock=self.responses, + responses_mock=responses, ) ds_os = ds.DataSourceOpenStack( settings.CFG_BUILTIN, @@ -319,6 +330,7 @@ def test_datasource(self, m_dhcp): self.assertIsNone(ds_os.vendordata_raw) m_dhcp.assert_not_called() + @responses.activate @test_helpers.mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network") @test_helpers.mock.patch( "cloudinit.net.ephemeral.maybe_perform_dhcp_discovery" @@ -331,7 +343,7 @@ def test_local_datasource(self, m_dhcp, m_net): EC2_FILES, EC2_META, OS_FILES, - responses_mock=self.responses, + responses_mock=responses, ) distro = mock.MagicMock() distro.get_tmp_exec_path = self.tmp_dir @@ -365,13 +377,14 @@ def test_local_datasource(self, m_dhcp, m_net): self.assertIsNone(ds_os_local.vendordata_raw) m_dhcp.assert_called_with(distro, "eth9", None) + @responses.activate def test_bad_datasource_meta(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("meta_data.json"): os_files[k] = "{" # some invalid json _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) ds_os = ds.DataSourceOpenStack( settings.CFG_BUILTIN, @@ -392,13 +405,14 @@ def test_bad_datasource_meta(self): r" http://(169.254.169.254|\[fe80::a9fe:a9fe\])", ) + @responses.activate def test_no_datasource(self): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("meta_data.json"): os_files.pop(k) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) ds_os = ds.DataSourceOpenStack( settings.CFG_BUILTIN, @@ -464,6 +478,7 @@ def test_network_config_cached(self): self.assertEqual(example_cfg, ds_os.network_config) m_convert_json.assert_not_called() + @responses.activate def test_disabled_datasource(self): os_files = copy.deepcopy(OS_FILES) os_meta = copy.deepcopy(OSTACK_META) @@ -474,7 +489,7 @@ def test_disabled_datasource(self): if k.endswith("meta_data.json"): os_files[k] = json.dumps(os_meta) _register_uris( - self.VERSION, {}, {}, os_files, responses_mock=self.responses + self.VERSION, {}, {}, os_files, responses_mock=responses ) ds_os = ds.DataSourceOpenStack( settings.CFG_BUILTIN, @@ -494,6 +509,7 @@ def test_disabled_datasource(self): self.assertFalse(found) self.assertIsNone(ds_os.version) + @responses.activate def test_wb__crawl_metadata_does_not_persist(self): """_crawl_metadata returns current metadata and does not cache.""" _register_uris( @@ -501,7 +517,7 @@ def test_wb__crawl_metadata_does_not_persist(self): EC2_FILES, EC2_META, OS_FILES, - responses_mock=self.responses, + responses_mock=responses, ) ds_os = ds.DataSourceOpenStack( settings.CFG_BUILTIN, @@ -790,7 +806,7 @@ def fake_dmi_read(dmi_key): m_proc_env.assert_called_with(1) -class TestMetadataReader(test_helpers.ResponsesTestCase): +class TestMetadataReader(test_helpers.CiTestCase): """Test the MetadataReader.""" burl = "http://169.254.169.254/" @@ -807,7 +823,7 @@ class TestMetadataReader(test_helpers.ResponsesTestCase): def register(self, path, body=None, status=200): content = body if not isinstance(body, str) else body.encode("utf-8") - self.responses.add( + responses.add( responses.GET, self.burl + "openstack" + path, status=status, @@ -828,6 +844,7 @@ def register_version(self, version, data): if "user_data" not in data: self.register("/%s/user_data" % version, "nodata", status=404) + @responses.activate def test__find_working_version(self): """Test a working version ignores unsupported.""" unsup = "2016-11-09" @@ -844,6 +861,7 @@ def test__find_working_version(self): openstack.MetadataReader(self.burl)._find_working_version(), ) + @responses.activate def test__find_working_version_uses_latest(self): """'latest' should be used if no supported versions.""" unsup1, unsup2 = ("2016-11-09", "2017-06-06") @@ -853,6 +871,7 @@ def test__find_working_version_uses_latest(self): openstack.MetadataReader(self.burl)._find_working_version(), ) + @responses.activate def test_read_v2_os_ocata(self): """Validate return value of read_v2 for os_ocata data.""" md = copy.deepcopy(self.md_base) diff --git a/tests/unittests/sources/test_scaleway.py b/tests/unittests/sources/test_scaleway.py index eefa6275dc5..bb1f2136de3 100644 --- a/tests/unittests/sources/test_scaleway.py +++ b/tests/unittests/sources/test_scaleway.py @@ -2,7 +2,6 @@ import json import socket -import sys from urllib.parse import SplitResult, urlsplit import requests @@ -12,7 +11,11 @@ from cloudinit import helpers, settings, sources from cloudinit.distros import ubuntu from cloudinit.sources import DataSourceScaleway -from tests.unittests.helpers import CiTestCase, ResponsesTestCase, mock +from tests.unittests.helpers import ( + CiTestCase, + mock, + responses_assert_call_count, +) class DataResponses: @@ -188,7 +191,7 @@ def _fix_mocking_url(url: str) -> str: ).geturl() -class TestDataSourceScaleway(ResponsesTestCase): +class TestDataSourceScaleway(CiTestCase): def setUp(self): tmp = self.tmp_dir() distro = ubuntu.Distro("", {}, {}) @@ -204,7 +207,7 @@ def setUp(self): # The trailing / at the end of the URL is needed to # workaround a bug in responses 3.6 which does not match # the URL otherwise. - self.responses.add_callback( + responses.add_callback( responses.GET, f"{url}/", callback=MetadataResponses.get_ok, @@ -223,18 +226,21 @@ def setUp(self): return_value="scalewaynic0", ) + @responses.activate def test_set_metadata_url_ipv4_ok(self): self.datasource._set_metadata_url([self.base_urls[0]]) self.assertTrue(self.base_urls[0] in self.datasource.metadata_url) + @responses.activate def test_set_metadata_url_ipv6_ok(self): self.datasource._set_metadata_url([self.base_urls[1]]) self.assertTrue(self.base_urls[1] in self.datasource.metadata_url) + @responses.activate @mock.patch( "cloudinit.sources.DataSourceScaleway.DataSourceScaleway" ".override_ds_detect" @@ -248,34 +254,34 @@ def test_ipv4_metadata_ok(self, dhcpv4, ds_detect): self.datasource._set_metadata_url([self.base_urls[0]]) - self.responses.reset() - self.responses.add_callback( + responses.reset() + responses.add_callback( responses.GET, f"{self.base_urls[0]}/", callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.base_urls[1]}/", callback=MetadataResponses.get_ok, ) # Use _fix_mocking_url to workaround py3.6 bug in responses - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(f"{self.base_urls[0]}/conf?format=json"), callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(f"{self.base_urls[1]}/conf?format=json"), callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.base_urls[0]}/user_data/cloud-init", callback=DataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.base_urls[0]}/vendor_data/cloud-init", callback=DataResponses.get_ok, @@ -309,6 +315,7 @@ def test_ipv4_metadata_ok(self, dhcpv4, ds_detect): self.assertIsNone(self.datasource.availability_zone) self.assertIsNone(self.datasource.region) + @responses.activate @mock.patch( "cloudinit.sources.DataSourceScaleway.DataSourceScaleway" ".override_ds_detect" @@ -324,46 +331,45 @@ def test_ipv4_metadata_timeout_ipv6_ok(self, dhcpv4, inet6, ds_detect): self.datasource._set_metadata_url([self.base_urls[0]]) - self.responses.reset() - self.responses.add_callback( + responses.reset() + responses.add_callback( responses.GET, f"{self.base_urls[0]}/", callback=ConnectTimeout, ) - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.base_urls[1]}/", callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(f"{self.base_urls[1]}/conf?format=json"), callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(f"{self.base_urls[1]}/user_data/cloud-init"), callback=DataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.base_urls[1]}/vendor_data/cloud-init", callback=DataResponses.get_ok, ) self.datasource.get_data() - if sys.version_info.minor >= 7: - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[0]}", - 1, - ) - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[1]}", - 1, - ) - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[1]}/conf?format=json", 1 - ) + responses_assert_call_count( + f"{self.datasource.metadata_urls[0]}", + 1, + ) + responses_assert_call_count( + f"{self.datasource.metadata_urls[1]}", + 1, + ) + responses_assert_call_count( + f"{self.datasource.metadata_urls[1]}/conf?format=json", 1 + ) self.assertEqual( self.datasource.get_instance_id(), @@ -392,6 +398,7 @@ def test_ipv4_metadata_timeout_ipv6_ok(self, dhcpv4, inet6, ds_detect): self.assertIsNone(self.datasource.availability_zone) self.assertIsNone(self.datasource.region) + @responses.activate @mock.patch( "cloudinit.sources.DataSourceScaleway.DataSourceScaleway" ".override_ds_detect" @@ -409,35 +416,28 @@ def test_ipv4_ipv6_metadata_timeout(self, dhcpv4, inet6, sleep, ds_detect): self.datasource._set_metadata_url([self.base_urls[0]]) # Remove callbacks defined at class initialization - self.responses.reset() - self.responses.add_callback( + responses.reset() + responses.add_callback( responses.GET, f"{self.base_urls[0]}/", callback=ConnectTimeout, ) - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.base_urls[1]}/", callback=ConnectTimeout, ) self.datasource.max_wait = 0 ret = self.datasource.get_data() - # assert_call_count is not available in responses for py3.6 - if sys.version_info.minor >= 7: - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[0]}", - 2, - ) - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[1]}", - 2, - ) + responses_assert_call_count(f"{self.datasource.metadata_urls[0]}", 2) + responses_assert_call_count(f"{self.datasource.metadata_urls[1]}", 2) self.assertFalse(ret) self.assertEqual(self.datasource.metadata, {}) self.assertIsNone(self.datasource.get_userdata_raw()) self.assertIsNone(self.datasource.get_vendordata_raw()) + @responses.activate @mock.patch( "cloudinit.sources.DataSourceScaleway.DataSourceScaleway" ".override_ds_detect" @@ -454,17 +454,17 @@ def test_metadata_ipv4_404(self, dhcpv4, ds_detect): # Make user and vendor data APIs return HTTP/404, which means there is # no user / vendor data for the server. - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(self.datasource.metadata_url), callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(self.datasource.userdata_url), callback=DataResponses.empty, ) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(self.datasource.vendordata_url), callback=DataResponses.empty, @@ -476,6 +476,7 @@ def test_metadata_ipv4_404(self, dhcpv4, ds_detect): self.assertIsNone(self.datasource.get_userdata_raw()) self.assertIsNone(self.datasource.get_vendordata_raw()) + @responses.activate @mock.patch("cloudinit.url_helper.time.sleep", lambda x: None) @mock.patch("cloudinit.sources.DataSourceScaleway.EphemeralDHCPv4") def test_metadata_connection_errors_legacy_ipv4_url(self, dhcpv4): @@ -490,9 +491,9 @@ def test_metadata_connection_errors_legacy_ipv4_url(self, dhcpv4): "http://169.254.42.42", ] - self.responses.reset() + responses.reset() with self.assertRaises(ConnectionError): - self.responses.add_callback( + responses.add_callback( responses.GET, f"{self.datasource.metadata_urls[0]}/", callback=ConnectionError, @@ -502,6 +503,7 @@ def test_metadata_connection_errors_legacy_ipv4_url(self, dhcpv4): self.assertIsNone(self.datasource.get_userdata_raw()) self.assertIsNone(self.datasource.get_vendordata_raw()) + @responses.activate @mock.patch( "cloudinit.sources.DataSourceScaleway.DataSourceScaleway" ".override_ds_detect" @@ -536,13 +538,13 @@ def test_metadata_connection_errors_two_urls( ] self.datasource.has_ipv4 = True - self.responses.reset() - self.responses.add_callback( + responses.reset() + responses.add_callback( responses.GET, self.datasource.metadata_urls[0], callback=ConnectionError, ) - self.responses.add_callback( + responses.add_callback( responses.GET, self.datasource.metadata_urls[1], callback=ConnectionError, @@ -551,18 +553,18 @@ def test_metadata_connection_errors_two_urls( self.datasource.get_data() # url_helper.wait_on_url tests both URL in list each time so # called twice for each URL - if sys.version_info.minor >= 7: - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[0]}", - 2, - ) - self.responses.assert_call_count( - f"{self.datasource.metadata_urls[1]}", - 2, - ) + responses_assert_call_count( + f"{self.datasource.metadata_urls[0]}", + 2, + ) + responses_assert_call_count( + f"{self.datasource.metadata_urls[1]}", + 2, + ) self.assertIsNone(self.datasource.get_userdata_raw()) self.assertIsNone(self.datasource.get_vendordata_raw()) + @responses.activate @mock.patch( "cloudinit.sources.DataSourceScaleway.DataSourceScaleway" ".override_ds_detect" @@ -578,12 +580,12 @@ def test_metadata_ipv4_rate_limit(self, sleep, dhcpv4, ds_detect): self.datasource._set_metadata_url([self.base_urls[0]]) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(self.datasource.metadata_url), callback=MetadataResponses.get_ok, ) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(self.datasource.vendordata_url), callback=DataResponses.empty, @@ -603,7 +605,7 @@ def _callback(request): return DataResponses.rate_limited(request) return DataResponses.get_ok(request) - self.responses.add_callback( + responses.add_callback( responses.GET, _fix_mocking_url(self.datasource.userdata_url), callback=_callback, From ff1d9f041d1fad0ac23935dcd6bab07cf9a73604 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 8 Jul 2025 11:37:06 -0500 Subject: [PATCH 014/150] docs: use correct jinja content-type (#6296) Fixes GH-6027 --- doc/rtd/explanation/format.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/rtd/explanation/format.rst b/doc/rtd/explanation/format.rst index f999a9ecd6e..9a27062f811 100644 --- a/doc/rtd/explanation/format.rst +++ b/doc/rtd/explanation/format.rst @@ -388,7 +388,7 @@ as binary data and so may be processed automatically. +--------------------+-----------------------------+-------------------------+ |Cloud config archive|#cloud-config-archive |text/cloud-config-archive| +--------------------+-----------------------------+-------------------------+ -|Jinja template |## template: jinja |text/jinja | +|Jinja template |## template: jinja |text/jinja2 | +--------------------+-----------------------------+-------------------------+ |Include file |#include |text/x-include-url | +--------------------+-----------------------------+-------------------------+ From de6ae26fe53f2ae2ebf9bed5c99d5b53d9bc014b Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 9 Jul 2025 15:33:59 +0200 Subject: [PATCH 015/150] test: drop FilesystemMockingTestCase from cc tests (#6282) --- tests/unittests/config/test_cc_disk_setup.py | 8 +- tests/unittests/config/test_cc_keyboard.py | 36 ++--- tests/unittests/config/test_cc_mcollective.py | 88 +++++------ tests/unittests/config/test_cc_resizefs.py | 2 +- tests/unittests/config/test_cc_resolv_conf.py | 90 +++++------ tests/unittests/config/test_cc_runcmd.py | 71 ++++----- .../config/test_cc_update_etc_hosts.py | 57 ++++--- tests/unittests/config/test_cc_write_files.py | 144 ++++++++++-------- .../config/test_cc_write_files_deferred.py | 35 +++-- .../unittests/config/test_cc_yum_add_repo.py | 51 ++----- .../config/test_cc_zypper_add_repo.py | 107 ++++++------- tests/unittests/conftest.py | 31 +++- 12 files changed, 341 insertions(+), 379 deletions(-) diff --git a/tests/unittests/config/test_cc_disk_setup.py b/tests/unittests/config/test_cc_disk_setup.py index 734f5e4315c..1fab32df712 100644 --- a/tests/unittests/config/test_cc_disk_setup.py +++ b/tests/unittests/config/test_cc_disk_setup.py @@ -3,6 +3,7 @@ import random import tempfile +from contextlib import ExitStack import pytest @@ -12,12 +13,7 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import ( - CiTestCase, - ExitStack, - mock, - skipUnlessJsonSchema, -) +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema class TestIsDiskUsed: diff --git a/tests/unittests/config/test_cc_keyboard.py b/tests/unittests/config/test_cc_keyboard.py index 7606b350e91..d0731a54075 100644 --- a/tests/unittests/config/test_cc_keyboard.py +++ b/tests/unittests/config/test_cc_keyboard.py @@ -14,11 +14,7 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import ( - FilesystemMockingTestCase, - populate_dir, - skipUnlessJsonSchema, -) +from tests.unittests.helpers import populate_dir, skipUnlessJsonSchema from tests.unittests.util import get_cloud @@ -82,17 +78,11 @@ def test_schema_validation(self, config, error_msg): validate_cloudconfig_schema(config, schema, strict=True) -class TestKeyboard(FilesystemMockingTestCase): - with_logs = True - - def setUp(self): - super(TestKeyboard, self).setUp() - self.root_d = self.tmp_dir() - self.root_d = self.reRoot() - +@pytest.mark.usefixtures("fake_filesystem") +class TestKeyboard: @mock.patch("cloudinit.distros.Distro.uses_systemd") @mock.patch("cloudinit.distros.subp.subp") - def test_systemd_linux_cmd(self, m_subp, m_uses_systemd, *args): + def test_systemd_linux_cmd(self, m_subp, m_uses_systemd): """Non-Debian systems run localectl""" cfg = {"keyboard": {"layout": "us", "variant": "us"}} layout = "us" @@ -132,7 +122,7 @@ def test_debian_linux_cmd(self, m_subp, m_write_file): ) @mock.patch("cloudinit.distros.subp.subp") - def test_alpine_linux_cmd(self, m_subp, *args): + def test_alpine_linux_cmd(self, m_subp, tmpdir): """Alpine Linux runs setup-keymap""" cfg = {"keyboard": {"layout": "us", "variant": "us"}} layout = "us" @@ -142,14 +132,15 @@ def test_alpine_linux_cmd(self, m_subp, *args): # Create a dummy directory and file for keymap keymap_dir = "/usr/share/bkeymaps/%s" % "us" keymap_file = "%s/%s.bmap.gz" % (keymap_dir, "us") - os.makedirs("%s%s" % (self.root_d, keymap_dir)) - populate_dir(self.root_d, {keymap_file: "# Test\n"}) + + os.makedirs(tmpdir.join(keymap_dir)) + populate_dir(str(tmpdir), {keymap_file: "# Test\n"}) cc_keyboard.handle("cc_keyboard", cfg, cloud, []) m_subp.assert_called_once_with(["setup-keymap", layout, variant]) @mock.patch("cloudinit.distros.subp.subp") - def test_alpine_linux_ignore_model(self, m_subp): + def test_alpine_linux_ignore_model(self, m_subp, caplog, tmpdir): """Alpine Linux ignores model setting""" cfg = { "keyboard": { @@ -164,14 +155,11 @@ def test_alpine_linux_ignore_model(self, m_subp): keymap_dir = "/usr/share/bkeymaps/%s" % "us" keymap_file = "%s/%s.bmap.gz" % (keymap_dir, "us") - os.makedirs("%s%s" % (self.root_d, keymap_dir)) - populate_dir(self.root_d, {keymap_file: "# Test\n"}) + os.makedirs(tmpdir.join(keymap_dir)) + populate_dir(str(tmpdir), {keymap_file: "# Test\n"}) cc_keyboard.handle("cc_keyboard", cfg, cloud, []) - assert ( - "Keyboard model is ignored for Alpine Linux." - in self.logs.getvalue() - ) + assert "Keyboard model is ignored for Alpine Linux." in caplog.text m_subp.assert_called_once_with( [ "setup-keymap", diff --git a/tests/unittests/config/test_cc_mcollective.py b/tests/unittests/config/test_cc_mcollective.py index 522b7858e3a..bd164018cb7 100644 --- a/tests/unittests/config/test_cc_mcollective.py +++ b/tests/unittests/config/test_cc_mcollective.py @@ -1,8 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging import os -import shutil -import tempfile from io import BytesIO import configobj @@ -46,22 +44,23 @@ """ -class TestConfig(t_help.FilesystemMockingTestCase): - def setUp(self): - super(TestConfig, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) - # "./": make os.path.join behave correctly with abs path as second arg - self.server_cfg = os.path.join( - self.tmp, "./" + cc_mcollective.SERVER_CFG - ) - self.pubcert_file = os.path.join( - self.tmp, "./" + cc_mcollective.PUBCERT_FILE - ) - self.pricert_file = os.path.join( - self.tmp, self.tmp, "./" + cc_mcollective.PRICERT_FILE - ) +@pytest.fixture +def server_cfg(tmp_path): + return str(tmp_path / cc_mcollective.SERVER_CFG) + + +@pytest.fixture +def pricert_file(tmp_path): + return str(tmp_path / cc_mcollective.PRICERT_FILE) + + +@pytest.fixture +def pubcert_file(tmp_path): + return str(tmp_path / cc_mcollective.PUBCERT_FILE) + +@pytest.mark.usefixtures("fake_filesystem") +class TestConfig: def test_basic_config(self): cfg = { "mcollective": { @@ -84,30 +83,27 @@ def test_basic_config(self): } expected = cfg["mcollective"]["conf"] - self.patchUtils(self.tmp) cc_mcollective.configure(cfg["mcollective"]["conf"]) contents = util.load_binary_file(cc_mcollective.SERVER_CFG) contents = configobj.ConfigObj(BytesIO(contents)) - self.assertEqual(expected, dict(contents)) + assert expected == dict(contents) - def test_existing_config_is_saved(self): + def test_existing_config_is_saved(self, server_cfg): cfg = {"loglevel": "warn"} - util.write_file(self.server_cfg, STOCK_CONFIG) - cc_mcollective.configure(config=cfg, server_cfg=self.server_cfg) - self.assertTrue(os.path.exists(self.server_cfg)) - self.assertTrue(os.path.exists(self.server_cfg + ".old")) - self.assertEqual( - util.load_text_file(self.server_cfg + ".old"), STOCK_CONFIG - ) + util.write_file(server_cfg, STOCK_CONFIG) + cc_mcollective.configure(config=cfg, server_cfg=server_cfg) + assert os.path.exists(server_cfg) + assert os.path.exists(server_cfg + ".old") + assert util.load_text_file(server_cfg + ".old") == STOCK_CONFIG - def test_existing_updated(self): + def test_existing_updated(self, server_cfg): cfg = {"loglevel": "warn"} - util.write_file(self.server_cfg, STOCK_CONFIG) - cc_mcollective.configure(config=cfg, server_cfg=self.server_cfg) - cfgobj = configobj.ConfigObj(self.server_cfg) - self.assertEqual(cfg["loglevel"], cfgobj["loglevel"]) + util.write_file(server_cfg, STOCK_CONFIG) + cc_mcollective.configure(config=cfg, server_cfg=server_cfg) + cfgobj = configobj.ConfigObj(server_cfg) + assert cfg["loglevel"] == cfgobj["loglevel"] - def test_certificats_written(self): + def test_certificats_written(self, pricert_file, pubcert_file, server_cfg): # check public-cert and private-cert keys in config get written cfg = { "loglevel": "debug", @@ -117,30 +113,26 @@ def test_certificats_written(self): cc_mcollective.configure( config=cfg, - server_cfg=self.server_cfg, - pricert_file=self.pricert_file, - pubcert_file=self.pubcert_file, + server_cfg=server_cfg, + pricert_file=pricert_file, + pubcert_file=pubcert_file, ) - found = configobj.ConfigObj(self.server_cfg) + found = configobj.ConfigObj(server_cfg) # make sure these didnt get written in - self.assertFalse("public-cert" in found) - self.assertFalse("private-cert" in found) + assert "public-cert" not in found + assert "private-cert" not in found # these need updating to the specified paths - self.assertEqual(found["plugin.ssl_server_public"], self.pubcert_file) - self.assertEqual(found["plugin.ssl_server_private"], self.pricert_file) + assert found["plugin.ssl_server_public"] == pubcert_file + assert found["plugin.ssl_server_private"] == pricert_file # and the security provider should be ssl - self.assertEqual(found["securityprovider"], "ssl") + assert found["securityprovider"] == "ssl" - self.assertEqual( - util.load_text_file(self.pricert_file), cfg["private-cert"] - ) - self.assertEqual( - util.load_text_file(self.pubcert_file), cfg["public-cert"] - ) + assert util.load_text_file(pricert_file) == cfg["private-cert"] + assert util.load_text_file(pubcert_file) == cfg["public-cert"] class TestHandler(t_help.TestCase): diff --git a/tests/unittests/config/test_cc_resizefs.py b/tests/unittests/config/test_cc_resizefs.py index 5a3057f40a2..8648f085629 100644 --- a/tests/unittests/config/test_cc_resizefs.py +++ b/tests/unittests/config/test_cc_resizefs.py @@ -423,7 +423,7 @@ def test_maybe_get_writable_device_path_non_block_on_container(self): def test_maybe_get_writable_device_path_returns_command_line_root(self): """When root device is UUID in kernel command_line, update devpath.""" - # XXX Long-term we want to use FilesystemMocking test to avoid + # XXX Long-term we want to use fake_filesystem test to avoid # touching os.stat. FakeStat = namedtuple( "FakeStat", ["st_mode", "st_size", "st_mtime"] diff --git a/tests/unittests/config/test_cc_resolv_conf.py b/tests/unittests/config/test_cc_resolv_conf.py index 58ca647b3dd..6bf67dda026 100644 --- a/tests/unittests/config/test_cc_resolv_conf.py +++ b/tests/unittests/config/test_cc_resolv_conf.py @@ -1,15 +1,12 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging -import os -import shutil -import tempfile from copy import deepcopy from unittest import mock import pytest -from cloudinit import cloud, distros, helpers, util +from cloudinit import cloud, helpers from cloudinit.config import cc_resolv_conf from cloudinit.config.cc_resolv_conf import generate_resolv_conf from cloudinit.config.schema import ( @@ -18,10 +15,7 @@ validate_cloudconfig_schema, ) from tests.helpers import cloud_init_project_dir -from tests.unittests.helpers import ( - FilesystemMockingTestCase, - skipUnlessJsonSchema, -) +from tests.unittests.helpers import skipUnlessJsonSchema from tests.unittests.util import MockDistro LOG = logging.getLogger(__name__) @@ -33,89 +27,77 @@ #\n\n""" -class TestResolvConf(FilesystemMockingTestCase): - with_logs = True +@pytest.mark.usefixtures("fake_filesystem") +class TestResolvConf: cfg = {"manage_resolv_conf": True, "resolv_conf": {}} - def setUp(self): - super(TestResolvConf, self).setUp() - self.tmp = tempfile.mkdtemp() - util.ensure_dir(os.path.join(self.tmp, "data")) - self.addCleanup(shutil.rmtree, self.tmp) - - def _fetch_distro(self, kind, conf=None): - cls = distros.fetch(kind) - paths = helpers.Paths({"cloud_dir": self.tmp}) - conf = {} if conf is None else conf - return cls(kind, conf, paths) - - def call_resolv_conf_handler(self, distro_name, conf, cc=None): - if not cc: - ds = None - distro = self._fetch_distro(distro_name, conf) - paths = helpers.Paths({"cloud_dir": self.tmp}) - cc = cloud.Cloud(ds, paths, {}, distro, None) + def call_resolv_conf_handler(self, distro, conf, paths): + ds = None + cc = cloud.Cloud(ds, paths, {}, distro, None) cc_resolv_conf.handle("cc_resolv_conf", conf, cc, []) @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_resolv_conf_systemd_resolved(self, m_render_to_file): - self.call_resolv_conf_handler("photon", self.cfg) + def test_resolv_conf_systemd_resolved( + self, m_render_to_file, Distro, paths + ): + dist = Distro("photon", self.cfg) + self.call_resolv_conf_handler(dist, self.cfg, paths) assert [ mock.call(mock.ANY, "/etc/systemd/resolved.conf", mock.ANY) ] == m_render_to_file.call_args_list @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_resolv_conf_no_param(self, m_render_to_file): + def test_resolv_conf_no_param(self, m_render_to_file, caplog, paths): tmp = deepcopy(self.cfg) - self.logs.truncate(0) tmp.pop("resolv_conf") - self.call_resolv_conf_handler("photon", tmp) + self.call_resolv_conf_handler("photon", tmp, paths) - self.assertIn( - "manage_resolv_conf True but no parameters provided", - self.logs.getvalue(), + assert ( + "manage_resolv_conf True but no parameters provided" in caplog.text ) assert [ mock.call(mock.ANY, "/etc/systemd/resolved.conf", mock.ANY) ] not in m_render_to_file.call_args_list @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_resolv_conf_manage_resolv_conf_false(self, m_render_to_file): + def test_resolv_conf_manage_resolv_conf_false( + self, m_render_to_file, caplog, Distro, paths + ): tmp = deepcopy(self.cfg) - self.logs.truncate(0) tmp["manage_resolv_conf"] = False - self.call_resolv_conf_handler("photon", tmp) - self.assertIn( - "'manage_resolv_conf' present but set to False", - self.logs.getvalue(), - ) + dist = Distro("photon", self.cfg) + self.call_resolv_conf_handler(dist, tmp, paths) + assert "'manage_resolv_conf' present but set to False" in caplog.text assert [ mock.call(mock.ANY, "/etc/systemd/resolved.conf", mock.ANY) ] not in m_render_to_file.call_args_list @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_resolv_conf_etc_resolv_conf(self, m_render_to_file): - self.call_resolv_conf_handler("rhel", self.cfg) + def test_resolv_conf_etc_resolv_conf( + self, m_render_to_file, Distro, paths + ): + dist = Distro("rhel", self.cfg) + self.call_resolv_conf_handler(dist, self.cfg, paths) assert [ mock.call(mock.ANY, "/etc/resolv.conf", mock.ANY) ] == m_render_to_file.call_args_list @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_resolv_conf_invalid_resolve_conf_fn(self, m_render_to_file): + def test_resolv_conf_invalid_resolve_conf_fn( + self, m_render_to_file, caplog, Distro, tmp_path + ): ds = None - distro = self._fetch_distro("rhel", self.cfg) - paths = helpers.Paths({"cloud_dir": self.tmp}) - cc = cloud.Cloud(ds, paths, {}, distro, None) + dist = Distro("rhel", self.cfg) + paths = helpers.Paths({"cloud_dir": str(tmp_path)}) + cc = cloud.Cloud(ds, paths, {}, dist, None) cc.distro.resolve_conf_fn = "bla" - self.logs.truncate(0) - self.call_resolv_conf_handler("rhel", self.cfg, cc) + cc_resolv_conf.handle("rhel", self.cfg, cc, []) - self.assertIn( - "No template found, not rendering resolve configs", - self.logs.getvalue(), + assert ( + "No template found, not rendering resolve configs" in caplog.text ) assert [ diff --git a/tests/unittests/config/test_cc_runcmd.py b/tests/unittests/config/test_cc_runcmd.py index d696aed1de7..bdf394a5122 100644 --- a/tests/unittests/config/test_cc_runcmd.py +++ b/tests/unittests/config/test_cc_runcmd.py @@ -7,81 +7,68 @@ import pytest -from cloudinit import helpers, subp, util +from cloudinit import helpers, util from cloudinit.config.cc_runcmd import handle from cloudinit.config.schema import ( SchemaValidationError, get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import ( - SCHEMA_EMPTY_ERROR, - FilesystemMockingTestCase, - skipUnlessJsonSchema, -) +from tests.unittests.helpers import SCHEMA_EMPTY_ERROR, skipUnlessJsonSchema from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) -class TestRuncmd(FilesystemMockingTestCase): +@pytest.fixture +def cloud(tmp_path): + paths = helpers.Paths({"scripts": str(tmp_path)}) + return get_cloud(paths=paths) - with_logs = True - def setUp(self): - super(TestRuncmd, self).setUp() - self.subp = subp.subp - self.new_root = self.tmp_dir() - self.patchUtils(self.new_root) - self.paths = helpers.Paths({"scripts": self.new_root}) +@pytest.mark.usefixtures("fake_filesystem") +class TestRuncmd: + # self.paths = helpers.Paths({"scripts": self.new_root}) - def test_handler_skip_if_no_runcmd(self): + def test_handler_skip_if_no_runcmd(self, caplog, cloud): """When the provided config doesn't contain runcmd, skip it.""" cfg: Dict[str, Any] = {} - mycloud = get_cloud(paths=self.paths) - handle("notimportant", cfg, mycloud, []) - self.assertIn( - "Skipping module named notimportant, no 'runcmd' key", - self.logs.getvalue(), + handle("notimportant", cfg, cloud, []) + assert ( + "Skipping module named notimportant, no 'runcmd' key" + in caplog.text ) + @pytest.mark.allow_subp_for("/bin/sh") @patch("cloudinit.util.shellify") - def test_runcmd_shellify_fails(self, cls): + def test_runcmd_shellify_fails(self, cls, cloud): """When shellify fails throw exception""" cls.side_effect = TypeError("patched shellify") valid_config = {"runcmd": ["echo 42"]} - cc = get_cloud(paths=self.paths) - with self.assertRaises(TypeError) as cm: - with self.allow_subp(["/bin/sh"]): - handle("cc_runcmd", valid_config, cc, []) - self.assertIn("Failed to shellify", str(cm.exception)) + with pytest.raises(TypeError, match="Failed to shellify"): + handle("cc_runcmd", valid_config, cloud, []) - def test_handler_invalid_command_set(self): + def test_handler_invalid_command_set(self, cloud): """Commands which can't be converted to shell will raise errors.""" invalid_config = {"runcmd": 1} - cc = get_cloud(paths=self.paths) - with self.assertRaises(TypeError) as cm: - handle("cc_runcmd", invalid_config, cc, []) - self.assertIn( - "Failed to shellify 1 into file" + with pytest.raises( + TypeError, + match="Failed to shellify 1 into file" " /var/lib/cloud/instances/iid-datasource-none/scripts/runcmd", - str(cm.exception), - ) + ): + handle("cc_runcmd", invalid_config, cloud, []) - def test_handler_write_valid_runcmd_schema_to_file(self): + def test_handler_write_valid_runcmd_schema_to_file(self, cloud, tmp_path): """Valid runcmd schema is written to a runcmd shell script.""" valid_config = {"runcmd": [["ls", "/"]]} - cc = get_cloud(paths=self.paths) - handle("cc_runcmd", valid_config, cc, []) + handle("cc_runcmd", valid_config, cloud, []) runcmd_file = os.path.join( - self.new_root, + tmp_path, "var/lib/cloud/instances/iid-datasource-none/scripts/runcmd", ) - self.assertEqual( - "#!/bin/sh\n'ls' '/'\n", util.load_text_file(runcmd_file) - ) + assert "#!/bin/sh\n'ls' '/'\n" == util.load_text_file(runcmd_file) file_stat = os.stat(runcmd_file) - self.assertEqual(0o700, stat.S_IMODE(file_stat.st_mode)) + assert 0o700 == stat.S_IMODE(file_stat.st_mode) @skipUnlessJsonSchema() diff --git a/tests/unittests/config/test_cc_update_etc_hosts.py b/tests/unittests/config/test_cc_update_etc_hosts.py index 6ede9954c1b..ebf6f599e1e 100644 --- a/tests/unittests/config/test_cc_update_etc_hosts.py +++ b/tests/unittests/config/test_cc_update_etc_hosts.py @@ -14,66 +14,75 @@ get_schema, validate_cloudconfig_schema, ) +from tests.helpers import cloud_init_project_dir from tests.unittests import helpers as t_help LOG = logging.getLogger(__name__) -class TestHostsFile(t_help.FilesystemMockingTestCase): - def setUp(self): - super(TestHostsFile, self).setUp() - self.tmp = self.tmp_dir() +@pytest.fixture(autouse=True) +def with_templates(tmp_path, fake_filesystem_hook): + shutil.copytree( + str(cloud_init_project_dir("templates")), + str(tmp_path / "templates"), + dirs_exist_ok=True, + ) + +@pytest.mark.usefixtures("fake_filesystem") +class TestHostsFile: def _fetch_distro(self, kind): cls = distros.fetch(kind) paths = helpers.Paths({}) return cls(kind, {}, paths) - def test_write_etc_hosts_suse_localhost(self): + def test_write_etc_hosts_suse_localhost(self, tmp_path): cfg = { "manage_etc_hosts": "localhost", "hostname": "cloud-init.test.us", } - os.makedirs("%s/etc/" % self.tmp) + os.makedirs(tmp_path / "etc/") hosts_content = "192.168.1.1 blah.blah.us blah\n" - fout = open("%s/etc/hosts" % self.tmp, "w") + etc_hosts = str(tmp_path / "etc/hosts") + fout = open(etc_hosts, "w") fout.write(hosts_content) fout.close() distro = self._fetch_distro("sles") - distro.hosts_fn = "%s/etc/hosts" % self.tmp + distro.hosts_fn = etc_hosts paths = helpers.Paths({}) ds = None cc = cloud.Cloud(ds, paths, {}, distro, None) - self.patchUtils(self.tmp) cc_update_etc_hosts.handle("test", cfg, cc, []) - contents = util.load_text_file("%s/etc/hosts" % self.tmp) - if "127.0.1.1\tcloud-init.test.us\tcloud-init" not in contents: - self.assertIsNone("No entry for 127.0.1.1 in etc/hosts") - if "192.168.1.1\tblah.blah.us\tblah" not in contents: - self.assertIsNone("Default etc/hosts content modified") + contents = util.load_text_file(etc_hosts) + assert ( + "127.0.1.1\tcloud-init.test.us\tcloud-init" in contents + ), "No entry for 127.0.1.1 in etc/hosts" + assert ( + "192.168.1.1\tblah.blah.us\tblah" in contents + ), "Default etc/hosts content modified" @t_help.skipUnlessJinja() - def test_write_etc_hosts_suse_template(self): + def test_write_etc_hosts_suse_template(self, tmp_path): cfg = { "manage_etc_hosts": "template", "hostname": "cloud-init.test.us", } shutil.copytree( - t_help.cloud_init_project_dir("templates"), - "%s/etc/cloud/templates" % self.tmp, + tmp_path / "templates", str(tmp_path / "etc/cloud/templates") ) distro = self._fetch_distro("sles") paths = helpers.Paths({}) - paths.template_tpl = "%s" % self.tmp + "/etc/cloud/templates/%s.tmpl" + paths.template_tpl = str(tmp_path / "etc/cloud/templates/%s.tmpl") ds = None cc = cloud.Cloud(ds, paths, {}, distro, None) - self.patchUtils(self.tmp) cc_update_etc_hosts.handle("test", cfg, cc, []) - contents = util.load_text_file("%s/etc/hosts" % self.tmp) - if "127.0.1.1 cloud-init.test.us cloud-init" not in contents: - self.assertIsNone("No entry for 127.0.1.1 in etc/hosts") - if "::1 cloud-init.test.us cloud-init" not in contents: - self.assertIsNone("No entry for 127.0.0.1 in etc/hosts") + contents = util.load_text_file(tmp_path / "etc/hosts") + assert ( + "127.0.1.1 cloud-init.test.us cloud-init" in contents + ), "No entry for 127.0.1.1 in etc/hosts" + assert ( + "::1 cloud-init.test.us cloud-init" in contents + ), "No entry for 127.0.0.1 in etc/hosts" class TestUpdateEtcHosts: diff --git a/tests/unittests/config/test_cc_write_files.py b/tests/unittests/config/test_cc_write_files.py index 7cae9a861e4..41844f926ef 100644 --- a/tests/unittests/config/test_cc_write_files.py +++ b/tests/unittests/config/test_cc_write_files.py @@ -5,8 +5,7 @@ import io import logging import re -import shutil -import tempfile +from unittest import mock import pytest import responses @@ -21,7 +20,6 @@ from tests.unittests.helpers import ( SCHEMA_EMPTY_ERROR, CiTestCase, - FilesystemMockingTestCase, skipUnlessJsonSchema, ) from tests.unittests.util import get_cloud @@ -65,39 +63,47 @@ } -class TestWriteFiles(FilesystemMockingTestCase): +OWNER = "root:root" +USER = "root" +GROUP = "root" - with_logs = True - owner = "root:root" - def setUp(self): - super(TestWriteFiles, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) +@pytest.fixture +def cloud(): + cc = get_cloud("ubuntu") + with mock.patch.object(cc.distro, "default_owner", OWNER): + yield cc + - def test_simple(self): - self.patchUtils(self.tmp) +@pytest.mark.usefixtures("fake_filesystem") +@mock.patch("cloudinit.config.cc_write_files.util.chownbyname") +class TestWriteFiles: + def test_simple(self, m_chownbyname): expected = "hello world\n" - filename = "/tmp/my.file" + filename = str("/tmp/my.file") write_files( "test_simple", [{"content": expected, "path": filename}], - self.owner, + OWNER, ) - self.assertEqual(util.load_text_file(filename), expected) + assert util.load_text_file(filename) == expected + assert [ + mock.call(mock.ANY, USER, GROUP) + ] == m_chownbyname.call_args_list - def test_empty(self): - self.patchUtils(self.tmp) - filename = "/tmp/my.file" + def test_empty(self, m_chownbyname): + filename = str("/tmp/my.file") write_files( "test_empty", [{"path": filename}], - self.owner, + OWNER, ) - self.assertEqual(util.load_text_file(filename), "") + assert util.load_text_file(filename) == "" + assert [ + mock.call(mock.ANY, USER, GROUP) + ] == m_chownbyname.call_args_list - def test_append(self): - self.patchUtils(self.tmp) + def test_append(self, m_chownbyname): existing = "hello " added = "world\n" expected = existing + added @@ -106,20 +112,26 @@ def test_append(self): write_files( "test_append", [{"content": added, "path": filename, "append": "true"}], - self.owner, + OWNER, ) - self.assertEqual(util.load_text_file(filename), expected) - - def test_yaml_binary(self): - self.patchUtils(self.tmp) - data = util.load_yaml(YAML_TEXT) - write_files("testname", data["write_files"], self.owner) + assert util.load_text_file(filename) == expected + assert [ + mock.call(mock.ANY, USER, GROUP) + ] == m_chownbyname.call_args_list + + def test_yaml_binary(self, m_chownbyname): + data_wrong_paths = util.load_yaml(YAML_TEXT) + data = [] + for content in data_wrong_paths["write_files"]: + content["path"] = content["path"] + data.append(content) + + write_files("testname", data, OWNER) for path, content in YAML_CONTENT_EXPECTED.items(): - self.assertEqual(util.load_text_file(path), content) - - def test_all_decodings(self): - self.patchUtils(self.tmp) + assert util.load_text_file(path) == content + assert 5 == m_chownbyname.call_count + def test_all_decodings(self, m_chownbyname): # build a 'files' array that has a dictionary of encodings # for 'gz', 'gzip', 'gz+base64' ... data = b"foobzr" @@ -148,19 +160,19 @@ def test_all_decodings(self): files.append(cur) expected.append((path, data)) - write_files("test_decoding", files, self.owner) + write_files("test_decoding", files, OWNER) for path, content in expected: - self.assertEqual(util.load_binary_file(path), content) + assert util.load_binary_file(path) == content # make sure we actually wrote *some* files. flen_expected = len(gz_aliases + gz_b64_aliases + b64_aliases) * len( datum ) - self.assertEqual(len(expected), flen_expected) + assert len(expected) == flen_expected + assert len(expected) == m_chownbyname.call_count - def test_handle_plain_text(self): - self.patchUtils(self.tmp) + def test_handle_plain_text(self, m_chownbyname, caplog, cloud): file_path = "/tmp/file-text-plain" content = "asdf" cfg = { @@ -173,15 +185,14 @@ def test_handle_plain_text(self): } ] } - cc = get_cloud("ubuntu") - handle("ignored", cfg, cc, []) + handle("ignored", cfg, cloud, []) assert content == util.load_text_file(file_path) - self.assertNotIn( - "Unknown encoding type text/plain", self.logs.getvalue() - ) + assert "Unknown encoding type text/plain" not in caplog.text + assert [ + mock.call(mock.ANY, USER, GROUP) + ] == m_chownbyname.call_args_list - def test_file_uri(self): - self.patchUtils(self.tmp) + def test_file_uri(self, m_chownbyname, cloud): src_path = "/tmp/file-uri" dst_path = "/tmp/file-uri-target" content = "asdf" @@ -194,15 +205,12 @@ def test_file_uri(self): } ] } - cc = get_cloud("ubuntu") - handle("ignored", cfg, cc, []) - self.assertEqual( - util.load_text_file(src_path), util.load_text_file(dst_path) - ) + handle("ignored", cfg, cloud, []) + assert util.load_text_file(src_path) == util.load_text_file(dst_path) + assert m_chownbyname.call_count @responses.activate - def test_http_uri(self): - self.patchUtils(self.tmp) + def test_http_uri(self, m_chownbyname, cloud): path = "/tmp/http-uri-target" url = "http://hostname/path" content = "more asdf" @@ -221,13 +229,14 @@ def test_http_uri(self): } ] } - cc = get_cloud("ubuntu") - handle("ignored", cfg, cc, []) - self.assertEqual(content, util.load_text_file(path)) - - def test_uri_fallback(self): - self.patchUtils(self.tmp) - src_path = "/tmp/INVALID" + handle("ignored", cfg, cloud, []) + assert content == util.load_text_file(path) + assert [ + mock.call(mock.ANY, USER, GROUP) + ] == m_chownbyname.call_args_list + + def test_uri_fallback(self, m_chownbyname, cloud): + src_path = "tmp/INVALID" dst_path = "/tmp/uri-fallback-target" content = "asdf" util.del_file(src_path) @@ -241,18 +250,19 @@ def test_uri_fallback(self): } ] } - cc = get_cloud("ubuntu") - handle("ignored", cfg, cc, []) - self.assertEqual(content, util.load_text_file(dst_path)) + handle("ignored", cfg, cloud, []) + assert content == util.load_text_file(dst_path) + assert [ + mock.call(mock.ANY, USER, GROUP) + ] == m_chownbyname.call_args_list - def test_deferred(self): - self.patchUtils(self.tmp) + def test_deferred(self, m_chownbyname, cloud): file_path = "/tmp/deferred.file" config = {"write_files": [{"path": file_path, "defer": True}]} - cc = get_cloud("ubuntu") - handle("cc_write_file", config, cc, []) - with self.assertRaises(FileNotFoundError): + handle("cc_write_file", config, cloud, []) + with pytest.raises(FileNotFoundError): util.load_text_file(file_path) + assert [] == m_chownbyname.call_args_list class TestDecodePerms(CiTestCase): diff --git a/tests/unittests/config/test_cc_write_files_deferred.py b/tests/unittests/config/test_cc_write_files_deferred.py index 1a217050cff..f12c9fd9135 100644 --- a/tests/unittests/config/test_cc_write_files_deferred.py +++ b/tests/unittests/config/test_cc_write_files_deferred.py @@ -1,8 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging -import shutil -import tempfile +from unittest import mock import pytest @@ -13,26 +12,19 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import ( - FilesystemMockingTestCase, - skipUnlessJsonSchema, -) +from tests.unittests.helpers import skipUnlessJsonSchema from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) -class TestWriteFilesDeferred(FilesystemMockingTestCase): - - with_logs = True +@pytest.mark.usefixtures("fake_filesystem") +class TestWriteFilesDeferred: - def setUp(self): - super(TestWriteFilesDeferred, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) + USER = "root" - def test_filtering_deferred_files(self): - self.patchUtils(self.tmp) + @mock.patch("cloudinit.config.cc_write_files.util.chownbyname") + def test_filtering_deferred_files(self, m_chownbyname): expected = "hello world\n" config = { "write_files": [ @@ -45,10 +37,17 @@ def test_filtering_deferred_files(self): ] } cc = get_cloud("ubuntu") - handle("cc_write_files_deferred", config, cc, []) - self.assertEqual(util.load_text_file("/tmp/deferred.file"), expected) - with self.assertRaises(FileNotFoundError): + # fake_filesytem's tree is owned by $USER:$USER + with mock.patch.object( + cc.distro, "default_owner", f"{self.USER}:{self.USER}" + ): + handle("cc_write_files_deferred", config, cc, []) + assert util.load_text_file("/tmp/deferred.file") == expected + with pytest.raises(FileNotFoundError): util.load_text_file("/tmp/not_deferred.file") + assert [ + mock.call(mock.ANY, self.USER, self.USER) + ] == m_chownbyname.call_args_list class TestWriteFilesDeferredSchema: diff --git a/tests/unittests/config/test_cc_yum_add_repo.py b/tests/unittests/config/test_cc_yum_add_repo.py index c77262f508f..5d4206310b4 100644 --- a/tests/unittests/config/test_cc_yum_add_repo.py +++ b/tests/unittests/config/test_cc_yum_add_repo.py @@ -3,8 +3,6 @@ import configparser import logging import re -import shutil -import tempfile import pytest @@ -20,12 +18,8 @@ LOG = logging.getLogger(__name__) -class TestConfig(helpers.FilesystemMockingTestCase): - def setUp(self): - super(TestConfig, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) - +@pytest.mark.usefixtures("fake_filesystem") +class TestConfig: def test_bad_config(self): cfg = { "yum_repos": { @@ -42,11 +36,9 @@ def test_bad_config(self): }, }, } - self.patchUtils(self.tmp) cc_yum_add_repo.handle("yum_add_repo", cfg, None, []) - self.assertRaises( - IOError, util.load_text_file, "/etc/yum.repos.d/epel_testing.repo" - ) + with pytest.raises(IOError): + util.load_text_file("/etc/yum.repos.d/epel_testing.repo") def test_metalink_config(self): cfg = { @@ -61,8 +53,6 @@ def test_metalink_config(self): }, }, } - self.patchUtils(self.tmp) - self.patchOS(self.tmp) cc_yum_add_repo.handle("yum_add_repo", cfg, None, []) contents = util.load_text_file("/etc/yum.repos.d/epel-testing.repo") parser = configparser.ConfigParser() @@ -78,12 +68,11 @@ def test_metalink_config(self): } } for section in expected: - self.assertTrue( - parser.has_section(section), - "Contains section {0}".format(section), + assert parser.has_section(section), "Contains section {0}".format( + section ) for k, v in expected[section].items(): - self.assertEqual(parser.get(section, k), v) + assert parser.get(section, k) == v def test_mirrorlist_config(self): cfg = { @@ -98,8 +87,6 @@ def test_mirrorlist_config(self): }, }, } - self.patchUtils(self.tmp) - self.patchOS(self.tmp) cc_yum_add_repo.handle("yum_add_repo", cfg, None, []) contents = util.load_text_file("/etc/yum.repos.d/epel-testing.repo") parser = configparser.ConfigParser() @@ -115,12 +102,11 @@ def test_mirrorlist_config(self): } } for section in expected: - self.assertTrue( - parser.has_section(section), - "Contains section {0}".format(section), + assert parser.has_section(section), "Contains section {0}".format( + section ) for k, v in expected[section].items(): - self.assertEqual(parser.get(section, k), v) + assert parser.get(section, k) == v def test_write_config(self): cfg = { @@ -135,8 +121,6 @@ def test_write_config(self): }, }, } - self.patchUtils(self.tmp) - self.patchOS(self.tmp) cc_yum_add_repo.handle("yum_add_repo", cfg, None, []) contents = util.load_text_file("/etc/yum.repos.d/epel-testing.repo") parser = configparser.ConfigParser() @@ -152,12 +136,11 @@ def test_write_config(self): } } for section in expected: - self.assertTrue( - parser.has_section(section), - "Contains section {0}".format(section), + assert parser.has_section(section), "Contains section {0}".format( + section ) for k, v in expected[section].items(): - self.assertEqual(parser.get(section, k), v) + assert parser.get(section, k) == v def test_write_config_array(self): cfg = { @@ -176,7 +159,6 @@ def test_write_config_array(self): } } } - self.patchUtils(self.tmp) cc_yum_add_repo.handle("yum_add_repo", cfg, None, []) contents = util.load_text_file( "/etc/yum.repos.d/puppetlabs-products.repo" @@ -196,12 +178,11 @@ def test_write_config_array(self): } } for section in expected: - self.assertTrue( - parser.has_section(section), - "Contains section {0}".format(section), + assert parser.has_section(section), "Contains section {0}".format( + section ) for k, v in expected[section].items(): - self.assertEqual(parser.get(section, k), v) + assert parser.get(section, k) == v class TestAddYumRepoSchema: diff --git a/tests/unittests/config/test_cc_zypper_add_repo.py b/tests/unittests/config/test_cc_zypper_add_repo.py index 21f033afd8a..aab273f63dc 100644 --- a/tests/unittests/config/test_cc_zypper_add_repo.py +++ b/tests/unittests/config/test_cc_zypper_add_repo.py @@ -5,6 +5,8 @@ import logging import os +import pytest + from cloudinit import util from cloudinit.config import cc_zypper_add_repo from tests.unittests import helpers @@ -12,11 +14,11 @@ LOG = logging.getLogger(__name__) -class TestConfig(helpers.FilesystemMockingTestCase): - def setUp(self): - super(TestConfig, self).setUp() - self.tmp = self.tmp_dir() - self.zypp_conf = "etc/zypp/zypp.conf" +ZYPP_CONF = "etc/zypp/zypp.conf" + + +@pytest.mark.usefixtures("fake_filesystem") +class TestConfig: def test_bad_repo_config(self): """Config has no baseurl, no file should be written""" @@ -25,16 +27,14 @@ def test_bad_repo_config(self): {"id": "foo", "name": "suse-test", "enabled": "1"}, ] } - self.patchUtils(self.tmp) cc_zypper_add_repo._write_repos(cfg["repos"], "/etc/zypp/repos.d") - self.assertRaises( - IOError, util.load_text_file, "/etc/zypp/repos.d/foo.repo" - ) + with pytest.raises(IOError): + util.load_text_file("/etc/zypp/repos.d/foo.repo") - def test_write_repos(self): + def test_write_repos(self, tmp_path): """Verify valid repos get written""" cfg = self._get_base_config_repos() - root_d = self.tmp_dir() + root_d = str(tmp_path) cc_zypper_add_repo._write_repos(cfg["zypper"]["repos"], root_d) repos = glob.glob("%s/*.repo" % root_d) expected_repos = ["testing-foo.repo", "testing-bar.repo"] @@ -46,7 +46,7 @@ def test_write_repos(self): assert 'Found repo with name "%s"; unexpected' % repo_name # Validation that the content gets properly written is in another test - def test_write_repo(self): + def test_write_repo(self, tmp_path): """Verify the content of a repo file""" cfg = { "repos": [ @@ -57,7 +57,7 @@ def test_write_repo(self): }, ] } - root_d = self.tmp_dir() + root_d = str(tmp_path) cc_zypper_add_repo._write_repos(cfg["repos"], root_d) contents = util.load_text_file("%s/testing-foo.repo" % root_d) parser = configparser.ConfigParser() @@ -71,21 +71,19 @@ def test_write_repo(self): } } for section in expected: - self.assertTrue( - parser.has_section(section), - "Contains section {0}".format(section), + assert parser.has_section(section), "Contains section {0}".format( + section ) for k, v in expected[section].items(): - self.assertEqual(parser.get(section, k), v) + assert parser.get(section, k) == v - def test_config_write(self): + def test_config_write(self, tmp_path): """Write valid configuration data""" cfg = {"config": {"download.deltarpm": "False", "reposdir": "foo"}} - root_d = self.tmp_dir() - helpers.populate_dir(root_d, {self.zypp_conf: "# Zypp config\n"}) - self.reRoot(root_d) + root_d = str(tmp_path) + helpers.populate_dir(root_d, {ZYPP_CONF: "# Zypp config\n"}) cc_zypper_add_repo._write_zypp_config(cfg["config"]) - cfg_out = os.path.join(root_d, self.zypp_conf) + cfg_out = os.path.join(root_d, ZYPP_CONF) contents = util.load_text_file(cfg_out) expected = [ "# Zypp config", @@ -95,9 +93,9 @@ def test_config_write(self): ] for item in contents.split("\n"): if item not in expected: - self.assertIsNone(item) + assert item is None - def test_config_write_skip_configdir(self): + def test_config_write_skip_configdir(self, tmp_path): """Write configuration but skip writing 'configdir' setting""" cfg = { "config": { @@ -106,11 +104,10 @@ def test_config_write_skip_configdir(self): "configdir": "bar", } } - root_d = self.tmp_dir() - helpers.populate_dir(root_d, {self.zypp_conf: "# Zypp config\n"}) - self.reRoot(root_d) + root_d = str(tmp_path) + helpers.populate_dir(root_d, {ZYPP_CONF: "# Zypp config\n"}) cc_zypper_add_repo._write_zypp_config(cfg["config"]) - cfg_out = os.path.join(root_d, self.zypp_conf) + cfg_out = os.path.join(root_d, ZYPP_CONF) contents = util.load_text_file(cfg_out) expected = [ "# Zypp config", @@ -120,48 +117,45 @@ def test_config_write_skip_configdir(self): ] for item in contents.split("\n"): if item not in expected: - self.assertIsNone(item) + assert item is None # Not finding teh right path for mocking :( # assert mock_logging.warning.called - def test_empty_config_section_no_new_data(self): + def test_empty_config_section_no_new_data(self, tmp_path): """When the config section is empty no new data should be written to zypp.conf""" cfg = self._get_base_config_repos() cfg["zypper"]["config"] = None - root_d = self.tmp_dir() - helpers.populate_dir(root_d, {self.zypp_conf: "# No data"}) - self.reRoot(root_d) + root_d = str(tmp_path) + helpers.populate_dir(root_d, {ZYPP_CONF: "# No data"}) cc_zypper_add_repo._write_zypp_config(cfg.get("config", {})) - cfg_out = os.path.join(root_d, self.zypp_conf) + cfg_out = os.path.join(root_d, ZYPP_CONF) contents = util.load_text_file(cfg_out) - self.assertEqual(contents, "# No data") + assert contents == "# No data" - def test_empty_config_value_no_new_data(self): + def test_empty_config_value_no_new_data(self, tmp_path): """When the config section is not empty but there are no values no new data should be written to zypp.conf""" cfg = self._get_base_config_repos() cfg["zypper"]["config"] = {"download.deltarpm": None} - root_d = self.tmp_dir() - helpers.populate_dir(root_d, {self.zypp_conf: "# No data"}) - self.reRoot(root_d) + root_d = str(tmp_path) + helpers.populate_dir(root_d, {ZYPP_CONF: "# No data"}) cc_zypper_add_repo._write_zypp_config(cfg.get("config", {})) - cfg_out = os.path.join(root_d, self.zypp_conf) + cfg_out = os.path.join(root_d, ZYPP_CONF) contents = util.load_text_file(cfg_out) - self.assertEqual(contents, "# No data") + assert contents == "# No data" - def test_handler_full_setup(self): + def test_handler_full_setup(self, tmp_path): """Test that the handler ends up calling the renderers""" cfg = self._get_base_config_repos() cfg["zypper"]["config"] = { "download.deltarpm": "False", } - root_d = self.tmp_dir() + root_d = str(tmp_path) os.makedirs("%s/etc/zypp/repos.d" % root_d) - helpers.populate_dir(root_d, {self.zypp_conf: "# Zypp config\n"}) - self.reRoot(root_d) + helpers.populate_dir(root_d, {ZYPP_CONF: "# Zypp config\n"}) cc_zypper_add_repo.handle("zypper_add_repo", cfg, None, []) - cfg_out = os.path.join(root_d, self.zypp_conf) + cfg_out = os.path.join(root_d, ZYPP_CONF) contents = util.load_text_file(cfg_out) expected = [ "# Zypp config", @@ -170,7 +164,7 @@ def test_handler_full_setup(self): ] for item in contents.split("\n"): if item not in expected: - self.assertIsNone(item) + assert item is None repos = glob.glob("%s/etc/zypp/repos.d/*.repo" % root_d) expected_repos = ["testing-foo.repo", "testing-bar.repo"] if len(repos) != 2: @@ -180,25 +174,24 @@ def test_handler_full_setup(self): if repo_name not in expected_repos: assert 'Found repo with name "%s"; unexpected' % repo_name - def test_no_config_section_no_new_data(self): + def test_no_config_section_no_new_data(self, tmp_path): """When there is no config section no new data should be written to zypp.conf""" cfg = self._get_base_config_repos() - root_d = self.tmp_dir() - helpers.populate_dir(root_d, {self.zypp_conf: "# No data"}) - self.reRoot(root_d) + root_d = str(tmp_path) + helpers.populate_dir(root_d, {ZYPP_CONF: "# No data"}) cc_zypper_add_repo._write_zypp_config(cfg.get("config", {})) - cfg_out = os.path.join(root_d, self.zypp_conf) + cfg_out = os.path.join(root_d, ZYPP_CONF) contents = util.load_text_file(cfg_out) - self.assertEqual(contents, "# No data") + assert contents == "# No data" - def test_no_repo_data(self): + def test_no_repo_data(self, tmp_path): """When there is no repo data nothing should happen""" - root_d = self.tmp_dir() - self.reRoot(root_d) + # fake_filesystem creates a `tmp` dir a more under tmp_path + root_d = str(tmp_path / "isolated") cc_zypper_add_repo._write_repos(None, root_d) content = glob.glob("%s/*" % root_d) - self.assertEqual(len(content), 0) + assert len(content) == 0 def _get_base_config_repos(self): """Basic valid repo configuration""" diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index 5d725cd56aa..b95bbfd1199 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -7,7 +7,7 @@ import pytest -from cloudinit import atomic_helper, helpers, lifecycle +from cloudinit import atomic_helper, distros, helpers, lifecycle from cloudinit import user_data as ud from cloudinit import util from cloudinit.gpg import GPG @@ -16,6 +16,15 @@ from tests.unittests.helpers import example_netdev, retarget_many_wrapper +@pytest.fixture +def Distro(paths): + def _get_distro(name, cfg=None): + cls = distros.fetch(name) + return cls(name, cfg or {}, paths) + + return _get_distro + + @pytest.fixture def m_gpg(): MockGPG = mock.Mock(spec=GPG) @@ -41,6 +50,7 @@ def m_gpg(): ("relpath", 1), ], os: [ + ("chown", 2), ("listdir", 1), ("mkdir", 1), ("rmdir", 1), @@ -78,8 +88,23 @@ def m_gpg(): @pytest.fixture -def fake_filesystem(mocker, tmpdir): - """Mocks fs functions to operate under `tmpdir`""" +def fake_filesystem_hook(): + """A hook to interact with the real filesystem before mocking it in + fake_filesystem. + + Fixtures needing to access the real filesystem in tests that use + fake_filesystem, can depend on this fixture to ensure they run before + fake_filesystem. + """ + + +@pytest.fixture +def fake_filesystem(mocker, tmpdir, fake_filesystem_hook): + """Mocks fs functions to operate under `tmpdir` + + This fixture is sorted after fix_cloud_init_hook to allow fixtures sorted + before fake_cloud_init_hook to access the real filesystem. + """ # This allows fake_filesystem to be used with production code that # creates temporary directories. Functions like TemporaryDirectory() # attempt to create a directory under "/tmp" assuming that it already From 2b6c7e6546af51e33612cb0899a29eec78102a7e Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 9 Jul 2025 14:43:18 -0500 Subject: [PATCH 016/150] fix: allow downstreams to strip invalid MTU (#6246) mtu: 0 is an invalid value within neptlan configuration, but cloud-init allowed it in previous releases. Since this also applies to v2 passthrough, we need strip the line outside of normal parsing. This commit adds a flag to allow cloud-init to strip it from downstreams to keep backwards compatibility. Fixes GH-6239 --- cloudinit/features.py | 10 ++++++ cloudinit/net/netplan.py | 25 ++++++++++++++ tests/unittests/net/test_netplan.py | 52 +++++++++++++++++++++++++++++ tests/unittests/test_data.py | 45 +++++++++++++------------ tests/unittests/test_features.py | 6 ++-- 5 files changed, 115 insertions(+), 23 deletions(-) diff --git a/cloudinit/features.py b/cloudinit/features.py index 2cc0791e17d..24edf880175 100644 --- a/cloudinit/features.py +++ b/cloudinit/features.py @@ -98,6 +98,16 @@ to modify the systemd unit files. """ +STRIP_INVALID_MTU = False +""" +If ``STRIP_INVALID_MTU`` is True, then cloud-init will strip invalid MTU +values from rendered v2 netplan configuration. Cloud-init allowed these values +prior to 24.2, so this flag is used to maintain compatibility with +previously generated network configurations. + +(This flag can be removed when Noble is no longer supported.) +""" + DEPRECATION_INFO_BOUNDARY = "devel" """ DEPRECATION_INFO_BOUNDARY is used by distros to configure at which upstream diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 6a0d7e40143..bce89bbd302 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -4,6 +4,7 @@ import io import logging import os +import re import textwrap from tempfile import SpooledTemporaryFile from typing import Callable, List, Optional @@ -265,6 +266,7 @@ def netplan_api_write_yaml_file(net_config_content: str) -> bool: CLOUDINIT_NETPLAN_FILE, ) return False + net_config_content = _maybe_strip_invalid_mtu(net_config_content) try: with SpooledTemporaryFile(mode="w") as f: f.write(net_config_content) @@ -294,6 +296,29 @@ def netplan_api_write_yaml_file(net_config_content: str) -> bool: return True +def _maybe_strip_invalid_mtu(net_config_content: str): + """Strip invalid MTU from the netplan config. + + This is a fix for https://github.com/canonical/cloud-init/issues/6239 + A 0 MTU value is NOT valid, but cloud-init accepted it prior to 24.2, + so rejecting it after c465de8 is a breaking change for existing releases. + """ + if features.STRIP_INVALID_MTU: + # Using regex here is admittedly not great, but this is post-processing + # of the netplan config, and we'd have to be dealing with some very + # gnarly yaml to get a multiline mtu: 0 entry. The alternative is + # another round trip of yaml parsing, which is more expensive. Unless + # there's a demonstrated need for proper yaml parsing, the added + # complexity does not seem worth it. + net_config_content = re.sub( + r"^\s*mtu:\s*0\s*\n", + "", + net_config_content, + flags=re.MULTILINE, + ) + return net_config_content + + def has_netplan_config_changed(cfg_file: str, content: str) -> bool: """Return True when new netplan config has changed vs previous.""" if not os.path.exists(cfg_file): diff --git a/tests/unittests/net/test_netplan.py b/tests/unittests/net/test_netplan.py index 86bb32b106c..26a4e48bc13 100644 --- a/tests/unittests/net/test_netplan.py +++ b/tests/unittests/net/test_netplan.py @@ -51,3 +51,55 @@ def test_no_netplan_python_api(self, caplog): "No netplan python module. Fallback to write" f" {netplan.CLOUDINIT_NETPLAN_FILE}" in caplog.text ) + + +SIMPLE_V2_CFG_MTU = """\ +network: + version: 2 + ethernets: + eno1: + match: + macaddress: 08:94:ef:51:ae:e0 + mtu: 0 + eno2: + match: + macaddress: 08:94:ef:51:ae:e1 + mtu: 100 +""" + + +REDACTED_V2_CFG_MTU = """\ +network: + version: 2 + ethernets: + eno1: + match: + macaddress: 08:94:ef:51:ae:e0 + eno2: + match: + macaddress: 08:94:ef:51:ae:e1 + mtu: 100 +""" + + +class TestMaybeStripInvalidMTU: + @pytest.mark.parametrize( + "netcfg,expected,strip_enabled", + ( + pytest.param( + SIMPLE_V2_CFG_MTU, + SIMPLE_V2_CFG_MTU, + False, + id="valid_mtu_unchanged", + ), + pytest.param( + SIMPLE_V2_CFG_MTU, + REDACTED_V2_CFG_MTU, + True, + id="invalid_mtu_redatcted", + ), + ), + ) + def test__strip_invalid_mtu(self, netcfg, expected, strip_enabled, mocker): + mocker.patch("cloudinit.features.STRIP_INVALID_MTU", strip_enabled) + assert expected == netplan._maybe_strip_invalid_mtu(netcfg) diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index 0184d4636c0..204765e4874 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -472,20 +472,7 @@ def test_mime_text_plain(self, init_tmp, caplog): init_tmp.paths.get_ipath("cloud_config"), "", 0o600 ) - # Since features are intended to be overridden downstream, mock them - # all here so new feature flags don't require a new change to this - # unit test. - @mock.patch.multiple( - "cloudinit.features", - ERROR_ON_USER_DATA_FAILURE=True, - ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES=True, - EXPIRE_APPLIES_TO_HASHED_USERS=False, - NETPLAN_CONFIG_ROOT_READ_ONLY=True, - DEPRECATION_INFO_BOUNDARY="devel", - NOCLOUD_SEED_URL_APPEND_FORWARD_SLASH=False, - APT_DEB822_SOURCE_LIST_FILE=True, - ) - def test_shellscript(self, init_tmp, tmpdir, caplog): + def test_shellscript(self, init_tmp, caplog): """Raw text starting #!/bin/sh is treated as script.""" script = "#!/bin/sh\necho hello\n" init_tmp.datasource = FakeDataSource(script) @@ -507,16 +494,32 @@ def test_shellscript(self, init_tmp, tmpdir, caplog): mock.call(init_tmp.paths.get_ipath("cloud_config"), "", 0o600), ] ) + + def test_expected_artifacts(self, init_tmp, tmpdir, caplog, mocker): + """Test combined_cloud_config and instance_data_sensitive contents.""" + init_tmp.datasource = FakeDataSource() + + mocker.patch("cloudinit.util.write_file") + mocker.patch.object(init_tmp, "_reset") + mocker.patch( + "cloudinit.features.get_features", + return_value={ + "ERROR_ON_USER_DATA_FAILURE": True, + "ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES": False, + "SOME_FAKE_FEATURE": True, + }, + ) + + with caplog.at_level(logging.WARNING): + init_tmp.fetch() + init_tmp.consume_data() + assert caplog.records == [] # No warnings + expected = { "features": { "ERROR_ON_USER_DATA_FAILURE": True, - "ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES": True, - "EXPIRE_APPLIES_TO_HASHED_USERS": False, - "NETPLAN_CONFIG_ROOT_READ_ONLY": True, - "DEPRECATION_INFO_BOUNDARY": "devel", - "NOCLOUD_SEED_URL_APPEND_FORWARD_SLASH": False, - "APT_DEB822_SOURCE_LIST_FILE": True, - "MANUAL_NETWORK_WAIT": True, + "ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES": False, + "SOME_FAKE_FEATURE": True, }, "system_info": { "default_user": {"name": "ubuntu"}, diff --git a/tests/unittests/test_features.py b/tests/unittests/test_features.py index c592f790fac..101f925a9c0 100644 --- a/tests/unittests/test_features.py +++ b/tests/unittests/test_features.py @@ -11,8 +11,8 @@ class TestGetFeatures: def test_feature_without_override(self): # Since features are intended to be overridden downstream, mock them - # all here so new feature flags don't require a new change to this - # unit test. + # all here so that downstream changes to features do not require + # changes to this test. with mock.patch.multiple( "cloudinit.features", ERROR_ON_USER_DATA_FAILURE=True, @@ -23,6 +23,7 @@ def test_feature_without_override(self): NOCLOUD_SEED_URL_APPEND_FORWARD_SLASH=False, APT_DEB822_SOURCE_LIST_FILE=True, MANUAL_NETWORK_WAIT=False, + STRIP_INVALID_MTU=False, ): assert { "ERROR_ON_USER_DATA_FAILURE": True, @@ -33,4 +34,5 @@ def test_feature_without_override(self): "APT_DEB822_SOURCE_LIST_FILE": True, "DEPRECATION_INFO_BOUNDARY": "devel", "MANUAL_NETWORK_WAIT": False, + "STRIP_INVALID_MTU": False, } == features.get_features() From 9ecb0db694542ad824a50343ec3490b2e1f986ab Mon Sep 17 00:00:00 2001 From: Bryan Fraschetti Date: Thu, 10 Jul 2025 13:38:34 -0400 Subject: [PATCH 017/150] docs: Document network device table and limitations (#6187) Highlight that the network device table printed to the terminal during boot is intended to be debug-level output rather than user-level information. Explain potential justifications for unexpected output and reference the debugging docs as a method of further investigation. Fixes: GH-6161 --- doc/rtd/explanation/index.rst | 1 + doc/rtd/explanation/net-device-info.rst | 42 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 doc/rtd/explanation/net-device-info.rst diff --git a/doc/rtd/explanation/index.rst b/doc/rtd/explanation/index.rst index 9e2b82a790d..b8f90b5cc67 100644 --- a/doc/rtd/explanation/index.rst +++ b/doc/rtd/explanation/index.rst @@ -25,3 +25,4 @@ knowledge and become better at using and configuring ``cloud-init``. failure_states.rst exported_errors.rst return_codes.rst + net-device-info.rst diff --git a/doc/rtd/explanation/net-device-info.rst b/doc/rtd/explanation/net-device-info.rst new file mode 100644 index 00000000000..6e0eb6cc5a6 --- /dev/null +++ b/doc/rtd/explanation/net-device-info.rst @@ -0,0 +1,42 @@ +.. _net-device-info: + +Net Device Info +=============== + +During boot, cloud-init prints a table to the serial console that summarizes +the information about detected network devices and their state at that +moment in time. A sample table appears as follows: + +.. spelling:word-list:: + :ignore-words: + Hw + fe + +.. csv-table:: + :align: center + :header-rows: 1 + + Device, Up, Address, Mask, Scope, Hw-Address + eth0, True, 10.70.162.47, 255.255.255.0, global, 00:16:3e:1d:76:9b + eth0, True, fe80::216:3eff:fe1d:769b/64, ., link, 00:16:3e:1d:76:9b + lo, True, 127.0.0.1, 255.0.0.0, host, . + lo, True, ::1/128, ., host, . + +This table may be useful to analyze scenarios where an instance fails +to boot. The table is not printed in the final stage and it does not +necessarily represent the network’s final state. + +Why does the table show interfaces as down? +---------------------------------------------- + +If the table shows interfaces as down, it means that they were not +activated by the system before the table was printed. There are a few +potential reasons this can occur. Some explanations include: the order +in which the distro starts system services results in network devices +being brought up after the table is printed, the supplied network +configuration either implicitly or explicitly does not bring up the +interfaces + +The recommended troubleshooting advice is to inspect the network from +the shell and confirm whether or not it matches your expected setup +and follow the :ref:`debugging instructions`. From 87e7481883dea5b5aa079297c46ca35846276670 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 10 Jul 2025 19:59:03 +0200 Subject: [PATCH 018/150] doc(ssdlc): Add hardening page (#6297) --- doc/rtd/explanation/hardening.rst | 84 +++++++++++++++++++++++++++++++ doc/rtd/explanation/index.rst | 1 + 2 files changed, 85 insertions(+) create mode 100644 doc/rtd/explanation/hardening.rst diff --git a/doc/rtd/explanation/hardening.rst b/doc/rtd/explanation/hardening.rst new file mode 100644 index 00000000000..06c9be3b4c9 --- /dev/null +++ b/doc/rtd/explanation/hardening.rst @@ -0,0 +1,84 @@ +Security Hardening +****************** + +Cloud-init's use case is automating cloud instance initialization, with support +across distributions and platforms. There are a myriad of ways to imrpove the +security posture of a cloud-init configured machine. + +Follow the security hardening guidelines provided by the OSes and cloud +platforms that your cloud-init configuration is targeting. + +Many cloud platforms provide SSH public keys in metadata which setup the +default user with the appropriate configured means of access using +SSH public/private key pairs. + + +Updated packages +================ + +To ensure the available security fixes are applied to you VMs images upon +launch, it is recommended by `Ubuntu security team guidelines`_ to update +the packages + +.. note:: + + Ubuntu cloud images are configured by default to enable unattended-upgrades, + thus this is resolved this issue when the update gets triggered. One can + still apply this recommendation to cloud that gap and update the packages + on first boot. + +.. code-block:: yaml + + #cloud-config + package_update: true + package_upgrade: true + + +No plain text passwords +======================= + +Most of the harmful security exposure comes when custom user-data presented +as ``#cloud-config`` or run scripts by the end-user at VM launch time which +provides credentials in the form of clear passwords or credentials encoded in +URLs for services. + + +It is recommended not to include plain-text passwords or credentials in any +``runcmd``, ``bootcmd``, or user-data scripts (e.g., ``#!/bin/bash``), as this +configuration user-data may be accessible to others on the local network +depending on the cloud platform's instance metadata service (IMDS). +Instead, retrieve credentials for service endpoints from a secure +vault or configuration management service such as Puppet, Chef, Ansible, +or SaltStack. + +While creating users with the +:ref:`Users and Groups module `, do not use the +``user.plain_text_passwd`` key with its associated value as plain text. +``hashed_passwd`` is a more secure alternative. + +Avoid plain text passwords with the +:ref:`Set Passwords `. + +Alternatives to user passwords +------------------------------ + +We recommend using the :ref:`SSH module ` with ``ssh_import_id`` or +``ssh_authorized_keys`` to import public SSH keys. + + + +More info on `managing SSH-keys for openssh-server`_. + +SSH Host keys +============= + +Cloud-init publishes the SSH host public keys generated to the serial console +which can be validated prior to any SSH client connection to the launched VM. + +It provides assurance that you are connecting to the virtual machine you +intended to launch, and not being intercepted by a man-in-the-middle (MITM) +attack. + + +.. _Ubuntu security team guidelines: https://documentation.ubuntu.com/server/explanation/security/security_suggestions/#keep-your-system-up-to-date +.. _managing SSH-keys for openssh-server: https://documentation.ubuntu.com/server/how-to/security/openssh-server/#ssh-keys diff --git a/doc/rtd/explanation/index.rst b/doc/rtd/explanation/index.rst index b8f90b5cc67..5465a838fb2 100644 --- a/doc/rtd/explanation/index.rst +++ b/doc/rtd/explanation/index.rst @@ -20,6 +20,7 @@ knowledge and become better at using and configuring ``cloud-init``. instancedata.rst vendordata.rst security.rst + hardening.rst analyze.rst kernel-command-line.rst failure_states.rst From 422573b66b6f6f2ae68b00b68e5a20f4cd398bfc Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 15 Jul 2025 14:54:47 -0500 Subject: [PATCH 019/150] docs: clarify what #include can include (#6309) The description includes `both base config and meta config`, but those terms aren't defined in the page, and can be thought to refer to the base config and meta-data configuration formats rather than user-data. Since the description already says "any kind of user-data format", just remove the clarification. --- doc/rtd/explanation/format.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/rtd/explanation/format.rst b/doc/rtd/explanation/format.rst index 9a27062f811..a1fd83dd8c0 100644 --- a/doc/rtd/explanation/format.rst +++ b/doc/rtd/explanation/format.rst @@ -157,9 +157,8 @@ Explanation ----------- An include file contains a list of URLs, one per line. Each of the URLs will -be read and their content can be any kind of user-data format, both base -config and meta config. If an error occurs reading a file the remaining files -will not be read. +be read and their content can be any kind of user-data format. If an error +occurs reading a file the remaining files will not be read. .. _user_data_formats-jinja: From 6873d635dfb4d3105c1d897a899c868d4738afcd Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 16 Jul 2025 15:19:31 +0200 Subject: [PATCH 020/150] test: drop some FilesystemMockingTestCases (#6284) --- tests/unittests/conftest.py | 2 + tests/unittests/distros/test__init__.py | 139 ++++++--------- tests/unittests/distros/test_bsd_utils.py | 4 +- tests/unittests/helpers.py | 13 ++ tests/unittests/runs/test_merge_run.py | 88 +++++----- tests/unittests/runs/test_simple_run.py | 170 +++++++++---------- tests/unittests/sources/test_azure.py | 2 +- tests/unittests/sources/test_azure_helper.py | 3 +- tests/unittests/sources/test_configdrive.py | 3 +- tests/unittests/sources/test_ibmcloud.py | 143 +++++++--------- tests/unittests/test__init__.py | 3 +- 11 files changed, 269 insertions(+), 301 deletions(-) diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index b95bbfd1199..af6e42488fa 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -95,6 +95,8 @@ def fake_filesystem_hook(): Fixtures needing to access the real filesystem in tests that use fake_filesystem, can depend on this fixture to ensure they run before fake_filesystem. + + See in action in tests/unittests/runs/test_simple_run.py. """ diff --git a/tests/unittests/distros/test__init__.py b/tests/unittests/distros/test__init__.py index 39583b1378c..2447d313e96 100644 --- a/tests/unittests/distros/test__init__.py +++ b/tests/unittests/distros/test__init__.py @@ -1,8 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. import os -import shutil -import tempfile from unittest import mock import pytest @@ -10,7 +8,6 @@ from cloudinit import distros, util from cloudinit.distros.ubuntu import Distro from cloudinit.net.dhcp import Dhcpcd, IscDhclient, Udhcpc -from tests.unittests import helpers M_PATH = "cloudinit.distros." @@ -54,22 +51,13 @@ gapmi = distros._get_arch_package_mirror_info -class TestGenericDistro(helpers.FilesystemMockingTestCase): - with_logs = True - - def setUp(self): - super(TestGenericDistro, self).setUp() - # Make a temp directoy for tests to use. - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) - +@pytest.mark.usefixtures("fake_filesystem") +class TestGenericDistro: def _write_load_doas(self, user, rules): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - if not os.path.exists(os.path.join(self.tmp, "etc")): - os.makedirs(os.path.join(self.tmp, "etc")) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) + if not os.path.exists("/etc"): + os.makedirs("/etc") d.write_doas_rules(user, rules) contents = util.load_text_file(d.doas_fn) return contents, cls, d @@ -77,10 +65,8 @@ def _write_load_doas(self, user, rules): def _write_load_sudoers(self, _user, rules): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - os.makedirs(os.path.join(self.tmp, "etc")) - os.makedirs(os.path.join(self.tmp, "etc", "sudoers.d")) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) + os.makedirs("/etc") + os.makedirs(os.path.join("/etc", "sudoers.d")) d.write_sudo_rules("harlowja", rules) contents = util.load_text_file(d.ci_sudoers_fn) return contents, cls, d @@ -98,7 +84,7 @@ def test_doas_ensure_rules(self): rules = ["permit nopass harlowja"] contents = self._write_load_doas("harlowja", rules)[0] expected = ["permit nopass harlowja"] - self.assertEqual(len(expected), self._count_in(expected, contents)) + assert len(expected) == self._count_in(expected, contents) def test_doas_ensure_rules_list(self): rules = [ @@ -112,7 +98,7 @@ def test_doas_ensure_rules_list(self): "permit nopass harlowja cmd pwd", "permit nopass harlowja cmd df", ] - self.assertEqual(len(expected), self._count_in(expected, contents)) + assert len(expected) == self._count_in(expected, contents) def test_doas_ensure_handle_duplicates(self): rules = [ @@ -129,7 +115,7 @@ def test_doas_ensure_handle_duplicates(self): "permit nopass harlowja cmd pwd", "permit nopass harlowja cmd df", ] - self.assertEqual(len(expected), self._count_in(expected, contents)) + assert len(expected) == self._count_in(expected, contents) def test_doas_ensure_new(self): rules = [ @@ -138,12 +124,11 @@ def test_doas_ensure_new(self): "permit nopass harlowja cmd df", ] contents = self._write_load_doas("harlowja", rules)[0] - self.assertIn("# Created by cloud-init v.", contents) - self.assertIn("harlowja", contents) - self.assertEqual(4, contents.count("harlowja")) + assert "# Created by cloud-init v." in contents + assert "harlowja" in contents + assert 4 == contents.count("harlowja") def test_doas_ensure_append(self): - self.patchUtils(self.tmp) util.write_file("/etc/doas.conf", "# root user\npermit nopass root\n") rules = [ "permit nopass harlowja cmd ls", @@ -151,22 +136,22 @@ def test_doas_ensure_append(self): "permit nopass harlowja cmd df", ] contents = self._write_load_doas("harlowja", rules)[0] - self.assertIn("root", contents) - self.assertEqual(2, contents.count("root")) - self.assertIn("harlowja", contents) - self.assertEqual(4, contents.count("harlowja")) + assert "root" in contents + assert 2 == contents.count("root") + assert "harlowja" in contents + assert 4 == contents.count("harlowja") def test_sudoers_ensure_rules(self): rules = "ALL=(ALL:ALL) ALL" contents = self._write_load_sudoers("harlowja", rules)[0] expected = ["harlowja ALL=(ALL:ALL) ALL"] - self.assertEqual(len(expected), self._count_in(expected, contents)) + assert len(expected) == self._count_in(expected, contents) not_expected = [ "harlowja A", "harlowja L", "harlowja L", ] - self.assertEqual(0, self._count_in(not_expected, contents)) + assert 0 == self._count_in(not_expected, contents) def test_sudoers_ensure_rules_list(self): rules = [ @@ -180,13 +165,13 @@ def test_sudoers_ensure_rules_list(self): "harlowja B-ALL=(ALL:ALL) ALL", "harlowja C-ALL=(ALL:ALL) ALL", ] - self.assertEqual(len(expected), self._count_in(expected, contents)) + assert len(expected) == self._count_in(expected, contents) not_expected = [ "harlowja A", "harlowja L", "harlowja L", ] - self.assertEqual(0, self._count_in(not_expected, contents)) + assert 0 == self._count_in(not_expected, contents) def test_sudoers_ensure_handle_duplicates(self): rules = [ @@ -203,123 +188,101 @@ def test_sudoers_ensure_handle_duplicates(self): "harlowja B-ALL=(ALL:ALL) ALL", "harlowja C-ALL=(ALL:ALL) ALL", ] - self.assertEqual(len(expected), self._count_in(expected, contents)) + assert len(expected) == self._count_in(expected, contents) not_expected = [ "harlowja A", "harlowja L", "harlowja L", ] - self.assertEqual(0, self._count_in(not_expected, contents)) + assert 0 == self._count_in(not_expected, contents) def test_sudoers_ensure_new(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) d.ensure_sudo_dir("/b") contents = util.load_text_file("/etc/sudoers") - self.assertIn("includedir /b", contents) - self.assertTrue(os.path.isdir("/b")) + assert "includedir /b" in contents + assert os.path.isdir("/b") - def test_sudoers_ensure_append(self): + def test_sudoers_ensure_append(self, caplog): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) util.write_file("/etc/sudoers", "josh, josh\n") d.ensure_sudo_dir("/b") contents = util.load_text_file("/etc/sudoers") - self.assertIn("includedir /b", contents) - self.assertTrue(os.path.isdir("/b")) - self.assertIn("josh", contents) - self.assertEqual(2, contents.count("josh")) - self.assertIn( - "Added '#includedir /b' to /etc/sudoers", self.logs.getvalue() - ) + assert "includedir /b" in contents + assert os.path.isdir("/b") + assert "josh" in contents + assert 2 == contents.count("josh") + assert "Added '#includedir /b' to /etc/sudoers" in caplog.text def test_sudoers_ensure_append_sudoer_file(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) util.write_file("/etc/sudoers", "josh, josh\n") d.ensure_sudo_dir("/b", "/etc/sudoers") contents = util.load_text_file("/etc/sudoers") - self.assertIn("includedir /b", contents) - self.assertTrue(os.path.isdir("/b")) - self.assertIn("josh", contents) - self.assertEqual(2, contents.count("josh")) + assert "includedir /b" in contents + assert os.path.isdir("/b") + assert "josh" in contents + assert 2 == contents.count("josh") - def test_usr_sudoers_ensure_new(self): + def test_usr_sudoers_ensure_new(self, caplog): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) util.write_file("/usr/etc/sudoers", "josh, josh\n") d.ensure_sudo_dir("/b") contents = util.load_text_file("/etc/sudoers") - self.assertIn("josh", contents) - self.assertEqual(2, contents.count("josh")) - self.assertIn("includedir /b", contents) - self.assertTrue(os.path.isdir("/b")) - self.assertIn( - "Using content from '/usr/etc/sudoers", self.logs.getvalue() - ) + assert "josh" in contents + assert 2 == contents.count("josh") + assert "includedir /b" in contents + assert os.path.isdir("/b") + assert "Using content from '/usr/etc/sudoers" in caplog.text def test_usr_sudoers_ensure_no_etc_create_when_include_in_usr_etc(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) util.write_file("/usr/etc/sudoers", "#includedir /b") d.ensure_sudo_dir("/b") - self.assertTrue(not os.path.exists("/etc/sudoers")) + assert not os.path.exists("/etc/sudoers") def test_sudoers_ensure_only_one_includedir(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) for char in ["#", "@"]: util.write_file("/etc/sudoers", "{}includedir /b".format(char)) d.ensure_sudo_dir("/b") contents = util.load_text_file("/etc/sudoers") - self.assertIn("includedir /b", contents) - self.assertTrue(os.path.isdir("/b")) - self.assertEqual(1, contents.count("includedir /b")) + assert "includedir /b" in contents + assert os.path.isdir("/b") + assert 1 == contents.count("includedir /b") def test_arch_package_mirror_info_unknown(self): """for an unknown arch, we should get back that with arch 'default'.""" arch_mirrors = gapmi(package_mirrors, arch="unknown") - self.assertEqual(unknown_arch_info, arch_mirrors) + assert unknown_arch_info == arch_mirrors def test_arch_package_mirror_info_known(self): arch_mirrors = gapmi(package_mirrors, arch="amd64") - self.assertEqual(package_mirrors[0], arch_mirrors) + assert package_mirrors[0] == arch_mirrors def test_systemd_in_use(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) os.makedirs("/run/systemd/system") - self.assertTrue(d.uses_systemd()) + assert d.uses_systemd() def test_systemd_not_in_use(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) - self.assertFalse(d.uses_systemd()) + assert not d.uses_systemd() def test_systemd_symlink(self): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) - self.patchOS(self.tmp) - self.patchUtils(self.tmp) os.makedirs("/run/systemd") os.symlink("/", "/run/systemd/system") - self.assertFalse(d.uses_systemd()) + assert not d.uses_systemd() @mock.patch("cloudinit.distros.debian.read_system_locale") def test_get_locale_ubuntu(self, m_locale): @@ -328,7 +291,7 @@ def test_get_locale_ubuntu(self, m_locale): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) locale = d.get_locale() - self.assertEqual("C.UTF-8", locale) + assert "C.UTF-8" == locale @mock.patch("cloudinit.distros.rhel.Distro._read_system_locale") def test_get_locale_rhel(self, m_locale): @@ -337,7 +300,7 @@ def test_get_locale_rhel(self, m_locale): cls = distros.fetch("rhel") d = cls("rhel", {}, None) locale = d.get_locale() - self.assertEqual("C.UTF-8", locale) + assert "C.UTF-8" == locale def test_expire_passwd_uses_chpasswd(self): """Test ubuntu.expire_passwd uses the passwd command.""" diff --git a/tests/unittests/distros/test_bsd_utils.py b/tests/unittests/distros/test_bsd_utils.py index ae2bdf5b7dc..285e0c3829c 100644 --- a/tests/unittests/distros/test_bsd_utils.py +++ b/tests/unittests/distros/test_bsd_utils.py @@ -1,7 +1,9 @@ # This file is part of cloud-init. See LICENSE file for license information. +from contextlib import ExitStack + import cloudinit.distros.bsd_utils as bsd_utils -from tests.unittests.helpers import CiTestCase, ExitStack, mock +from tests.unittests.helpers import CiTestCase, mock RC_FILE = """ if something; then diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index c44ec79c3f6..81d15ea5e7d 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -392,6 +392,19 @@ def reRooted(self, root=None): self.patched_funcs.close() +def replicate_test_root(example_root, target_root): + real_root = resourceLocation() + real_root = os.path.join(real_root, "roots", example_root) + for dir_path, _dirnames, filenames in os.walk(real_root): + real_path = dir_path + make_path = rebase_path(real_path[len(real_root) :], target_root) + util.ensure_dir(make_path) + for f in filenames: + real_path = os.path.abspath(os.path.join(real_path, f)) + make_path = os.path.abspath(os.path.join(make_path, f)) + shutil.copy(real_path, make_path) + + def responses_assert_call_count(url: str, count: int) -> bool: """Focal and older have a version of responses which does not carry this attribute. This can be removed when focal diff --git a/tests/unittests/runs/test_merge_run.py b/tests/unittests/runs/test_merge_run.py index 7cd43c63122..876f8c69db5 100644 --- a/tests/unittests/runs/test_merge_run.py +++ b/tests/unittests/runs/test_merge_run.py @@ -1,57 +1,59 @@ # This file is part of cloud-init. See LICENSE file for license information. import os -import shutil -import tempfile + +import pytest from cloudinit import safeyaml, stages, util from cloudinit.config.modules import Modules from cloudinit.settings import PER_INSTANCE from tests.unittests import helpers +from tests.unittests.helpers import replicate_test_root + +@pytest.mark.usefixtures("fake_filesystem_hook") +@pytest.fixture(autouse=True) +def user_data(tmp_path): + replicate_test_root("simple_ubuntu", str(tmp_path)) + return helpers.readResource("user_data.1.txt") -class TestMergeRun(helpers.FilesystemMockingTestCase): - def _patchIn(self, root): - self.patchOS(root) - self.patchUtils(root) - def test_none_ds(self): - new_root = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, new_root) - self.replicateTestRoot("simple_ubuntu", new_root) - cfg = { - "datasource_list": ["None"], - "cloud_init_modules": ["write_files"], - "system_info": { - "paths": {"run_dir": new_root}, - "package_mirrors": [ - { - "arches": ["i386", "amd64", "blah"], - "failsafe": { - "primary": "http://my.archive.mydomain.com/ubuntu", - "security": ( - "http://my.security.mydomain.com/ubuntu" - ), - }, - "search": {"primary": [], "security": []}, +@pytest.fixture(autouse=True) +def cfg(tmp_path, mocker): + mocker.patch("cloudinit.util.os.chown") + new_root = str(tmp_path) + cfg = { + "datasource_list": ["None"], + "cloud_init_modules": ["write_files"], + "system_info": { + "paths": {"run_dir": new_root}, + "package_mirrors": [ + { + "arches": ["i386", "amd64", "blah"], + "failsafe": { + "primary": "http://my.archive.mydomain.com/ubuntu", + "security": ("http://my.security.mydomain.com/ubuntu"), }, - ], - }, - } - ud = helpers.readResource("user_data.1.txt") - cloud_cfg = safeyaml.dumps(cfg) - util.ensure_dir(os.path.join(new_root, "etc", "cloud")) - util.write_file( - os.path.join(new_root, "etc", "cloud", "cloud.cfg"), cloud_cfg - ) - self._patchIn(new_root) + "search": {"primary": [], "security": []}, + }, + ], + }, + } + cloud_cfg = safeyaml.dumps(cfg) + util.ensure_dir(os.path.join(new_root, "etc", "cloud")) + util.write_file( + os.path.join(new_root, "etc", "cloud", "cloud.cfg"), cloud_cfg + ) + - # Now start verifying whats created +@pytest.mark.usefixtures("fake_filesystem") +class TestMergeRun: + def test_none_ds(self, user_data): initer = stages.Init() initer.read_cfg() initer.initialize() initer.fetch() - initer.datasource.userdata_raw = ud + initer.datasource.userdata_raw = user_data initer.instancify() initer.update() initer.cloudify().run( @@ -61,13 +63,13 @@ def test_none_ds(self): freq=PER_INSTANCE, ) mirrors = initer.distro.get_option("package_mirrors") - self.assertEqual(1, len(mirrors)) + assert 1 == len(mirrors) mirror = mirrors[0] - self.assertEqual(mirror["arches"], ["i386", "amd64", "blah"]) + assert mirror["arches"] == ["i386", "amd64", "blah"] mods = Modules(initer) (which_ran, failures) = mods.run_section("cloud_init_modules") - self.assertFalse(failures) - self.assertTrue(os.path.exists("/etc/blah.ini")) - self.assertIn("write_files", which_ran) + assert not failures + assert os.path.exists("/etc/blah.ini") + assert "write_files" in which_ran contents = util.load_text_file("/etc/blah.ini") - self.assertEqual(contents, "blah") + assert contents == "blah" diff --git a/tests/unittests/runs/test_simple_run.py b/tests/unittests/runs/test_simple_run.py index e8737ea2d26..93493e72499 100644 --- a/tests/unittests/runs/test_simple_run.py +++ b/tests/unittests/runs/test_simple_run.py @@ -3,48 +3,52 @@ import copy import os +import pytest + from cloudinit import atomic_helper, safeyaml, stages, util from cloudinit.config.modules import Modules from cloudinit.settings import PER_INSTANCE from cloudinit.sources import NetworkConfigSource -from tests.unittests import helpers - - -class TestSimpleRun(helpers.FilesystemMockingTestCase): - - with_logs = True - - def setUp(self): - super(TestSimpleRun, self).setUp() - self.new_root = self.tmp_dir() - self.replicateTestRoot("simple_ubuntu", self.new_root) - - # Seed cloud.cfg file for our tests - self.cfg = { - "datasource_list": ["None"], - "runcmd": ["ls /etc"], # test ALL_DISTROS - "spacewalk": {}, # test non-ubuntu distros module definition - "system_info": { - "paths": {"run_dir": self.new_root}, - "distro": "ubuntu", +from tests.unittests.helpers import replicate_test_root + + +@pytest.mark.usefixtures("fake_filesystem_hook") +@pytest.fixture(autouse=True) +def replicate_root(tmp_path): + replicate_test_root("simple_ubuntu", str(tmp_path)) + + +@pytest.fixture(autouse=True) +def cfg(mocker, tmp_path): + new_root = str(tmp_path) + _cfg = { + "datasource_list": ["None"], + "runcmd": ["ls /etc"], # test ALL_DISTROS + "spacewalk": {}, # test non-ubuntu distros module definition + "system_info": { + "paths": {"run_dir": new_root}, + "distro": "ubuntu", + }, + "write_files": [ + { + "path": "/etc/blah.ini", + "content": "blah", + "permissions": 0o755, }, - "write_files": [ - { - "path": "/etc/blah.ini", - "content": "blah", - "permissions": 0o755, - }, - ], - "cloud_init_modules": ["write_files", "spacewalk", "runcmd"], - } - cloud_cfg = safeyaml.dumps(self.cfg) - util.ensure_dir(os.path.join(self.new_root, "etc", "cloud")) - util.write_file( - os.path.join(self.new_root, "etc", "cloud", "cloud.cfg"), cloud_cfg - ) - self.patchOS(self.new_root) - self.patchUtils(self.new_root) - + ], + "cloud_init_modules": ["write_files", "spacewalk", "runcmd"], + } + cloud_cfg = safeyaml.dumps(_cfg) + util.ensure_dir(os.path.join(new_root, "etc", "cloud")) + util.write_file( + os.path.join(new_root, "etc", "cloud", "cloud.cfg"), cloud_cfg + ) + mocker.patch("cloudinit.util.os.chown") + return _cfg + + +@pytest.mark.usefixtures("fake_filesystem") +class TestSimpleRun: def test_none_ds_populates_var_lib_cloud(self): """Init and run_section default behavior creates appropriate dirs.""" # Now start verifying whats created @@ -56,31 +60,30 @@ def test_none_ds_populates_var_lib_cloud(self): def fake_network_config(): return netcfg, NetworkConfigSource.FALLBACK - self.assertFalse(os.path.exists("/var/lib/cloud")) + assert not os.path.exists("/var/lib/cloud") initer = stages.Init() initer.read_cfg() initer.initialize() - self.assertTrue(os.path.exists("/var/lib/cloud")) + assert os.path.exists("/var/lib/cloud") for d in ["scripts", "seed", "instances", "handlers", "sem", "data"]: - self.assertTrue(os.path.isdir(os.path.join("/var/lib/cloud", d))) + assert os.path.isdir(os.path.join("/var/lib/cloud", d)) initer.fetch() - self.assertFalse(os.path.islink("var/lib/cloud/instance")) + assert not os.path.islink("var/lib/cloud/instance") iid = initer.instancify() - self.assertEqual(iid, "iid-datasource-none") + assert iid == "iid-datasource-none" initer.update() - self.assertTrue(os.path.islink("var/lib/cloud/instance")) + assert os.path.islink("var/lib/cloud/instance") initer._find_networking_config = fake_network_config - self.assertFalse( - os.path.exists("/var/lib/cloud/instance/network-config.json") + assert not os.path.exists( + "/var/lib/cloud/instance/network-config.json" ) initer.apply_network_config(False) - self.assertEqual( - f"{atomic_helper.json_dumps(netcfg)}\n", - util.load_text_file("/var/lib/cloud/instance/network-config.json"), + assert f"{atomic_helper.json_dumps(netcfg)}\n" == util.load_text_file( + "/var/lib/cloud/instance/network-config.json" ) - def test_none_ds_runs_modules_which_do_not_define_distros(self): + def test_none_ds_runs_modules_which_do_not_define_distros(self, caplog): """Any modules which do not define a distros attribute are run.""" initer = stages.Init() initer.read_cfg() @@ -97,18 +100,19 @@ def test_none_ds_runs_modules_which_do_not_define_distros(self): mods = Modules(initer) (which_ran, failures) = mods.run_section("cloud_init_modules") - self.assertFalse(failures) - self.assertTrue(os.path.exists("/etc/blah.ini")) - self.assertIn("write_files", which_ran) + assert not failures + assert os.path.exists("/etc/blah.ini") + assert "write_files" in which_ran contents = util.load_text_file("/etc/blah.ini") - self.assertEqual(contents, "blah") - self.assertNotIn( + assert contents == "blah" + assert ( "Skipping modules ['write_files'] because they are not verified on" - " distro 'ubuntu'", - self.logs.getvalue(), + " distro 'ubuntu'" not in caplog.text ) - def test_none_ds_skips_modules_which_define_unmatched_distros(self): + def test_none_ds_skips_modules_which_define_unmatched_distros( + self, caplog + ): """Skip modules which define distros which don't match the current.""" initer = stages.Init() initer.read_cfg() @@ -125,15 +129,14 @@ def test_none_ds_skips_modules_which_define_unmatched_distros(self): mods = Modules(initer) (which_ran, failures) = mods.run_section("cloud_init_modules") - self.assertFalse(failures) - self.assertIn( + assert not failures + assert ( "Skipping modules 'spacewalk' because they are not verified on" - " distro 'ubuntu'", - self.logs.getvalue(), + " distro 'ubuntu'" in caplog.text ) - self.assertNotIn("spacewalk", which_ran) + assert "spacewalk" not in which_ran - def test_none_ds_runs_modules_which_distros_all(self): + def test_none_ds_runs_modules_which_distros_all(self, caplog): """Skip modules which define distros attribute as supporting 'all'. This is done in the module with the declaration: @@ -154,25 +157,22 @@ def test_none_ds_runs_modules_which_distros_all(self): mods = Modules(initer) (which_ran, failures) = mods.run_section("cloud_init_modules") - self.assertFalse(failures) - self.assertIn("runcmd", which_ran) - self.assertNotIn( + assert not failures + assert "runcmd" in which_ran + assert ( "Skipping modules 'runcmd' because they are not verified on" - " distro 'ubuntu'", - self.logs.getvalue(), + " distro 'ubuntu'" not in caplog.text ) - def test_none_ds_forces_run_via_unverified_modules(self): + def test_none_ds_forces_run_via_unverified_modules(self, caplog, cfg): """run_section forced skipped modules by using unverified_modules.""" # re-write cloud.cfg with unverified_modules override - cfg = copy.deepcopy(self.cfg) + cfg = copy.deepcopy(cfg) cfg["unverified_modules"] = ["spacewalk"] # Would have skipped cloud_cfg = safeyaml.dumps(cfg) - util.ensure_dir(os.path.join(self.new_root, "etc", "cloud")) - util.write_file( - os.path.join(self.new_root, "etc", "cloud", "cloud.cfg"), cloud_cfg - ) + util.ensure_dir(os.path.join("/etc", "cloud")) + util.write_file(os.path.join("/etc", "cloud", "cloud.cfg"), cloud_cfg) initer = stages.Init() initer.read_cfg() @@ -189,24 +189,20 @@ def test_none_ds_forces_run_via_unverified_modules(self): mods = Modules(initer) (which_ran, failures) = mods.run_section("cloud_init_modules") - self.assertFalse(failures) - self.assertIn("spacewalk", which_ran) - self.assertIn( - "running unverified_modules: 'spacewalk'", self.logs.getvalue() - ) + assert not failures + assert "spacewalk" in which_ran + assert "running unverified_modules: 'spacewalk'" in caplog.text - def test_none_ds_run_with_no_config_modules(self): + def test_none_ds_run_with_no_config_modules(self, cfg): """run_section will report no modules run when none are configured.""" # re-write cloud.cfg with unverified_modules override - cfg = copy.deepcopy(self.cfg) + cfg = copy.deepcopy(cfg) # Represent empty configuration in /etc/cloud/cloud.cfg cfg["cloud_init_modules"] = None cloud_cfg = safeyaml.dumps(cfg) - util.ensure_dir(os.path.join(self.new_root, "etc", "cloud")) - util.write_file( - os.path.join(self.new_root, "etc", "cloud", "cloud.cfg"), cloud_cfg - ) + util.ensure_dir(os.path.join("/etc", "cloud")) + util.write_file(os.path.join("/etc", "cloud", "cloud.cfg"), cloud_cfg) initer = stages.Init() initer.read_cfg() @@ -223,5 +219,5 @@ def test_none_ds_run_with_no_config_modules(self): mods = Modules(initer) (which_ran, failures) = mods.run_section("cloud_init_modules") - self.assertFalse(failures) - self.assertEqual([], which_ran) + assert not failures + assert [] == which_ran diff --git a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py index a3c5bad6a92..f59fcf2d611 100644 --- a/tests/unittests/sources/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -7,6 +7,7 @@ import os import stat import xml.etree.ElementTree as ET +from contextlib import ExitStack from pathlib import Path import passlib.hash @@ -28,7 +29,6 @@ ) from tests.unittests.helpers import ( CiTestCase, - ExitStack, example_netdev, mock, populate_dir, diff --git a/tests/unittests/sources/test_azure_helper.py b/tests/unittests/sources/test_azure_helper.py index 190aee71693..a6b2b4e5b4e 100644 --- a/tests/unittests/sources/test_azure_helper.py +++ b/tests/unittests/sources/test_azure_helper.py @@ -4,6 +4,7 @@ import os import re import unittest +from contextlib import ExitStack from textwrap import dedent from xml.etree import ElementTree as ET from xml.sax.saxutils import escape, unescape @@ -17,7 +18,7 @@ from cloudinit.sources.helpers import azure as azure_helper from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim from cloudinit.util import load_text_file -from tests.unittests.helpers import CiTestCase, ExitStack, mock +from tests.unittests.helpers import CiTestCase, mock from tests.unittests.sources.test_azure import construct_ovf_env from tests.unittests.util import MockDistro diff --git a/tests/unittests/sources/test_configdrive.py b/tests/unittests/sources/test_configdrive.py index 6e97b992bac..a649df8950a 100644 --- a/tests/unittests/sources/test_configdrive.py +++ b/tests/unittests/sources/test_configdrive.py @@ -2,13 +2,14 @@ import json import os +from contextlib import ExitStack from copy import copy, deepcopy from cloudinit import helpers, settings, util from cloudinit.net import eni, network_state from cloudinit.sources import DataSourceConfigDrive as ds from cloudinit.sources.helpers import openstack -from tests.unittests.helpers import CiTestCase, ExitStack, mock, populate_dir +from tests.unittests.helpers import CiTestCase, mock, populate_dir PUBKEY = "ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n" EC2_META = { diff --git a/tests/unittests/sources/test_ibmcloud.py b/tests/unittests/sources/test_ibmcloud.py index 37c2594dce3..7154703c147 100644 --- a/tests/unittests/sources/test_ibmcloud.py +++ b/tests/unittests/sources/test_ibmcloud.py @@ -5,6 +5,8 @@ import json from textwrap import dedent +import pytest + from cloudinit import util from cloudinit.helpers import Paths from cloudinit.sources import DataSourceIBMCloud as ibm @@ -66,36 +68,34 @@ def test_id_template_live_metadata(self, m_blkid, m_is_prov, _m_xen): """identify TEMPLATE_LIVE_METADATA.""" m_blkid.return_value = self.blkid_metadata m_is_prov.return_value = False - self.assertEqual( - (ibm.Platforms.TEMPLATE_LIVE_METADATA, "/dev/xvdh1"), - ibm.get_ibm_platform(), - ) + assert ( + ibm.Platforms.TEMPLATE_LIVE_METADATA, + "/dev/xvdh1", + ) == ibm.get_ibm_platform() def test_id_template_prov_metadata(self, m_blkid, m_is_prov, _m_xen): """identify TEMPLATE_PROVISIONING_METADATA.""" m_blkid.return_value = self.blkid_metadata m_is_prov.return_value = True - self.assertEqual( - (ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, "/dev/xvdh1"), - ibm.get_ibm_platform(), - ) + assert ( + ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, + "/dev/xvdh1", + ) == ibm.get_ibm_platform() def test_id_template_prov_nodata(self, m_blkid, m_is_prov, _m_xen): """identify TEMPLATE_PROVISIONING_NODATA.""" m_blkid.return_value = self.blkid_base m_is_prov.return_value = True - self.assertEqual( - (ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, None), - ibm.get_ibm_platform(), - ) + assert ( + ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, + None, + ) == ibm.get_ibm_platform() def test_id_os_code(self, m_blkid, m_is_prov, _m_xen): """Identify OS_CODE.""" m_blkid.return_value = self.blkid_oscode m_is_prov.return_value = False - self.assertEqual( - (ibm.Platforms.OS_CODE, "/dev/xvdh"), ibm.get_ibm_platform() - ) + assert (ibm.Platforms.OS_CODE, "/dev/xvdh") == ibm.get_ibm_platform() def test_id_os_code_must_match_uuid(self, m_blkid, m_is_prov, _m_xen): """Test against false positive on openstack with non-ibm UUID.""" @@ -103,7 +103,7 @@ def test_id_os_code_must_match_uuid(self, m_blkid, m_is_prov, _m_xen): blkid["/dev/xvdh"]["UUID"] = "9999-9999" m_blkid.return_value = blkid m_is_prov.return_value = False - self.assertEqual((None, None), ibm.get_ibm_platform()) + assert (None, None) == ibm.get_ibm_platform() @mock.patch(D_PATH + "_read_system_uuid", return_value=None) @@ -235,7 +235,7 @@ def test_provisioning_md(self, m_platform, m_sysuuid): ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, "/dev/xvdh", ) - self.assertIsNone(ibm.read_md()) + assert ibm.read_md() is None def test_provisioning_no_metadata(self, m_platform, m_sysuuid): """Provisioning env with no metadata disk should return None.""" @@ -243,12 +243,12 @@ def test_provisioning_no_metadata(self, m_platform, m_sysuuid): ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, None, ) - self.assertIsNone(ibm.read_md()) + assert ibm.read_md() is None def test_provisioning_not_ibm(self, m_platform, m_sysuuid): """Provisioning env but not identified as IBM should return None.""" m_platform.return_value = (None, None) - self.assertIsNone(ibm.read_md()) + assert ibm.read_md() is None def test_template_live(self, m_platform, m_sysuuid): """Template live environment should be identified.""" @@ -273,14 +273,12 @@ def test_template_live(self, m_platform, m_sysuuid): ret = ibm.read_md() if ret is None: # this is needed for mypy - ensures ret is not None - self.fail("read_md returned None unexpectedly") - self.assertEqual(ibm.Platforms.TEMPLATE_LIVE_METADATA, ret["platform"]) - self.assertEqual(tmpdir, ret["source"]) - self.assertEqual(self.userdata, ret["userdata"]) - self.assertEqual( - self._get_expected_metadata(self.template_md), ret["metadata"] - ) - self.assertEqual(self.sysuuid, ret["system-uuid"]) + pytest.fail("read_md returned None unexpectedly") + assert ibm.Platforms.TEMPLATE_LIVE_METADATA == ret["platform"] + assert tmpdir == ret["source"] + assert self.userdata == ret["userdata"] + assert self._get_expected_metadata(self.template_md) == ret["metadata"] + assert self.sysuuid == ret["system-uuid"] def test_os_code_live(self, m_platform, m_sysuuid): """Verify an os_code metadata path.""" @@ -301,13 +299,11 @@ def test_os_code_live(self, m_platform, m_sysuuid): ret = ibm.read_md() if ret is None: # this is needed for mypy - ensures ret is not None - self.fail("read_md returned None unexpectedly") - self.assertEqual(ibm.Platforms.OS_CODE, ret["platform"]) - self.assertEqual(tmpdir, ret["source"]) - self.assertEqual(self.userdata, ret["userdata"]) - self.assertEqual( - self._get_expected_metadata(self.oscode_md), ret["metadata"] - ) + pytest.fail("read_md returned None unexpectedly") + assert ibm.Platforms.OS_CODE == ret["platform"] + assert tmpdir == ret["source"] + assert self.userdata == ret["userdata"] + assert self._get_expected_metadata(self.oscode_md) == ret["metadata"] def test_os_code_live_no_userdata(self, m_platform, m_sysuuid): """Verify os_code without user-data.""" @@ -325,69 +321,60 @@ def test_os_code_live_no_userdata(self, m_platform, m_sysuuid): ret = ibm.read_md() if ret is None: # this is needed for mypy - ensures ret is not None - self.fail("read_md returned None unexpectedly") - self.assertEqual(ibm.Platforms.OS_CODE, ret["platform"]) - self.assertEqual(tmpdir, ret["source"]) - self.assertIsNone(ret["userdata"]) - self.assertEqual( - self._get_expected_metadata(self.oscode_md), ret["metadata"] - ) + pytest.fail("read_md returned None unexpectedly") + assert ibm.Platforms.OS_CODE == ret["platform"] + assert tmpdir == ret["source"] + assert ret["userdata"] is None + assert self._get_expected_metadata(self.oscode_md) == ret["metadata"] -class TestIsIBMProvisioning(test_helpers.FilesystemMockingTestCase): +@pytest.mark.usefixtures("fake_filesystem") +class TestIsIBMProvisioning: """Test the _is_ibm_provisioning method.""" inst_log = "/root/swinstall.log" prov_cfg = "/root/provisioningConfiguration.cfg" boot_ref = "/proc/1/environ" - with_logs = True - - def _call_with_root(self, rootd): - self.reRoot(rootd) - return ibm._is_ibm_provisioning() def test_no_config(self): """No provisioning config means not provisioning.""" - self.assertFalse(self._call_with_root(self.tmp_dir())) + assert not ibm._is_ibm_provisioning() - def test_config_only(self): + def test_config_only(self, tmp_path): """A provisioning config without a log means provisioning.""" - rootd = self.tmp_dir() - test_helpers.populate_dir(rootd, {self.prov_cfg: "key=value"}) - self.assertTrue(self._call_with_root(rootd)) + test_helpers.populate_dir(str(tmp_path), {self.prov_cfg: "key=value"}) + assert ibm._is_ibm_provisioning() - def test_config_with_old_log(self): + def test_config_with_old_log(self, caplog, tmp_path): """A config with a log from previous boot is not provisioning.""" - rootd = self.tmp_dir() data = { self.prov_cfg: ("key=value\nkey2=val2\n", -10), self.inst_log: ("log data\n", -30), self.boot_ref: ("PWD=/", 0), } - test_helpers.populate_dir_with_ts(rootd, data) - self.assertFalse(self._call_with_root(rootd=rootd)) - self.assertIn("from previous boot", self.logs.getvalue()) + test_helpers.populate_dir_with_ts(str(tmp_path), data) + assert not ibm._is_ibm_provisioning() + assert "from previous boot" in caplog.text - def test_config_with_new_log(self): + def test_config_with_new_log(self, caplog, tmp_path): """A config with a log from this boot is provisioning.""" - rootd = self.tmp_dir() data = { self.prov_cfg: ("key=value\nkey2=val2\n", -10), self.inst_log: ("log data\n", 30), self.boot_ref: ("PWD=/", 0), } - test_helpers.populate_dir_with_ts(rootd, data) - self.assertTrue(self._call_with_root(rootd=rootd)) - self.assertIn("from current boot", self.logs.getvalue()) + test_helpers.populate_dir_with_ts(str(tmp_path), data) + assert ibm._is_ibm_provisioning() + assert "from current boot" in caplog.text - def test_config_and_log_no_reference(self): + def test_config_and_log_no_reference(self, caplog, tmp_path): """If the config and log existed, but no reference, assume not.""" - rootd = self.tmp_dir() test_helpers.populate_dir( - rootd, {self.prov_cfg: "key=value", self.inst_log: "log data\n"} + str(tmp_path), + {self.prov_cfg: "key=value", self.inst_log: "log data\n"}, ) - self.assertFalse(self._call_with_root(rootd=rootd)) - self.assertIn("no reference file", self.logs.getvalue()) + assert not ibm._is_ibm_provisioning() + assert "no reference file" in caplog.text class TestDataSourceIBMCloud(test_helpers.CiTestCase): @@ -402,7 +389,7 @@ def setUp(self): def test_get_data_false(self): """When read_md returns None, get_data returns False.""" with mock.patch(D_PATH + "read_md", return_value=None): - self.assertFalse(self.ds.get_data()) + assert not self.ds.get_data() def test_get_data_processes_read_md(self): """get_data processes and caches content returned by read_md.""" @@ -416,13 +403,13 @@ def test_get_data_processes_read_md(self): "vendordata": "vd", } with mock.patch(D_PATH + "read_md", return_value=md): - self.assertTrue(self.ds.get_data()) - self.assertEqual("src", self.ds.source) - self.assertEqual("plat", self.ds.platform) - self.assertEqual({}, self.ds.metadata) - self.assertEqual("ud", self.ds.userdata_raw) - self.assertEqual("net", self.ds.network_json) - self.assertEqual("uuid", self.ds.system_uuid) - self.assertEqual("ibmcloud", self.ds.cloud_name) - self.assertEqual("ibmcloud", self.ds.platform_type) - self.assertEqual("plat (src)", self.ds.subplatform) + assert self.ds.get_data() + assert "src" == self.ds.source + assert "plat" == self.ds.platform + assert {} == self.ds.metadata + assert "ud" == self.ds.userdata_raw + assert "net" == self.ds.network_json + assert "uuid" == self.ds.system_uuid + assert "ibmcloud" == self.ds.cloud_name + assert "ibmcloud" == self.ds.platform_type + assert "plat (src)" == self.ds.subplatform diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index 10cacf4b9e5..501009e3d1c 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -4,12 +4,13 @@ import os import shutil import tempfile +from contextlib import ExitStack import pytest from cloudinit import handlers, helpers, settings, url_helper, util from cloudinit.cmd import main -from tests.unittests.helpers import ExitStack, TestCase, mock +from tests.unittests.helpers import TestCase, mock class FakeModule(handlers.Handler): From 0fbea98bbf43721f8d44075563bf65770ade2c9d Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 16 Jul 2025 15:23:30 +0200 Subject: [PATCH 021/150] test: drop FilesystemMockingTestCase in smartos (#6291) GH-5760 --- tests/unittests/sources/test_smartos.py | 930 ++++++++++++------------ 1 file changed, 481 insertions(+), 449 deletions(-) diff --git a/tests/unittests/sources/test_smartos.py b/tests/unittests/sources/test_smartos.py index fbb7ee625a6..e73b7c4d37e 100644 --- a/tests/unittests/sources/test_smartos.py +++ b/tests/unittests/sources/test_smartos.py @@ -21,13 +21,13 @@ import re import signal import stat -import unittest import uuid from binascii import crc32 +from collections import namedtuple +import pytest import serial -from cloudinit import helpers as c_helpers from cloudinit.atomic_helper import b64e from cloudinit.event import EventScope, EventType from cloudinit.sources import DataSourceSmartOS @@ -41,12 +41,7 @@ ) from cloudinit.subp import ProcessExecutionError, subp, which from cloudinit.util import write_file -from tests.unittests.helpers import ( - CiTestCase, - FilesystemMockingTestCase, - mock, - skipIf, -) +from tests.unittests.helpers import mock, skipIf DSMOS = "cloudinit.sources.DataSourceSmartOS" SDC_NICS = json.loads( @@ -404,7 +399,7 @@ def exists(self): return True def open_transport(self): - assert not self._is_open + assert self._is_open is False self._is_open = True def close_transport(self): @@ -412,188 +407,178 @@ def close_transport(self): self._is_open = False -class TestSmartOSDataSource(FilesystemMockingTestCase): - jmc_cfact = None - get_smartos_environ = None +@pytest.fixture +def legacy_user_d(tmp_path): + legacy_user_dir = str(tmp_path / "legacy_user_tmp") + os.mkdir(legacy_user_dir) + return legacy_user_dir - def setUp(self): - super(TestSmartOSDataSource, self).setUp() - - self.add_patch(DSMOS + ".get_smartos_environ", "get_smartos_environ") - self.add_patch(DSMOS + ".jmc_client_factory", "jmc_cfact") - self.legacy_user_d = self.tmp_path("legacy_user_tmp") - os.mkdir(self.legacy_user_d) - self.add_patch( - DSMOS + ".LEGACY_USER_D", - "m_legacy_user_d", - autospec=False, - new=self.legacy_user_d, - ) - self.add_patch( - DSMOS + ".identify_file", - "m_identify_file", - return_value="text/plain", - ) - def _get_ds( - self, - mockdata=None, - mode=DataSourceSmartOS.SMARTOS_ENV_KVM, - sys_cfg=None, - ds_cfg=None, - ): - self.jmc_cfact.return_value = PsuedoJoyentClient(mockdata) - self.get_smartos_environ.return_value = mode +@pytest.fixture +def m_jmc_client_factory(mocker): + return mocker.patch( + DSMOS + ".jmc_client_factory", + return_value=PsuedoJoyentClient(MOCK_RETURNS), + ) - tmpd = self.tmp_dir() - dirs = { - "cloud_dir": self.tmp_path("cloud_dir", tmpd), - "run_dir": self.tmp_path("run_dir"), - } - for d in dirs.values(): - os.mkdir(d) - paths = c_helpers.Paths(dirs) - if sys_cfg is None: - sys_cfg = {} +@pytest.fixture +def mocks(legacy_user_d, mocker, m_jmc_client_factory): + mocker.patch( + DSMOS + ".get_smartos_environ", + return_value=DataSourceSmartOS.SMARTOS_ENV_KVM, + ) + mocker.patch( + DSMOS + ".LEGACY_USER_D", + autospec=False, + new=legacy_user_d, + ) + mocker.patch( + DSMOS + ".identify_file", + return_value="text/plain", + ) + mocker.patch("cloudinit.net.activators.subp.subp", return_value=("", "")) - if ds_cfg is not None: - sys_cfg["datasource"] = sys_cfg.get("datasource", {}) - sys_cfg["datasource"]["SmartOS"] = ds_cfg - return DataSourceSmartOS.DataSourceSmartOS( - sys_cfg, distro=None, paths=paths - ) +def _get_ds(paths, ds_cfg=None): + sys_cfg = {} + if ds_cfg is not None: + sys_cfg["datasource"] = {} + sys_cfg["datasource"]["SmartOS"] = ds_cfg + + return DataSourceSmartOS.DataSourceSmartOS( + sys_cfg, distro=None, paths=paths + ) + + +@pytest.fixture +def ds(paths): + return lambda: _get_ds(paths) + + +@pytest.mark.usefixtures("fake_filesystem", "mocks") +class TestSmartOSDataSource: + jmc_cfact = None + get_smartos_environ = None - def test_no_base64(self): + def test_no_base64(self, paths): ds_cfg = {"no_base64_decode": ["test_var1"], "all_base": True} - dsrc = self._get_ds(ds_cfg=ds_cfg) + dsrc = _get_ds(paths, ds_cfg=ds_cfg) ret = dsrc.get_data() - self.assertTrue(ret) + assert ret is True - def test_uuid(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_uuid(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["sdc:uuid"], dsrc.metadata["instance-id"] - ) + assert ret is True + assert MOCK_RETURNS["sdc:uuid"] == dsrc.metadata["instance-id"] - def test_platform_info(self): + def test_platform_info(self, ds, m_jmc_client_factory): """All platform-related attributes are properly set.""" - dsrc = self._get_ds(mockdata=MOCK_RETURNS) - self.assertEqual("joyent", dsrc.cloud_name) - self.assertEqual("joyent", dsrc.platform_type) - self.assertEqual("serial (/dev/ttyS1)", dsrc.subplatform) + dsrc = ds() + assert "joyent" == dsrc.cloud_name + assert "joyent" == dsrc.platform_type + assert "serial (/dev/ttyS1)" == dsrc.subplatform - def test_root_keys(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_root_keys(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["root_authorized_keys"], dsrc.metadata["public-keys"] + assert ret is True + assert ( + MOCK_RETURNS["root_authorized_keys"] + == dsrc.metadata["public-keys"] ) - def test_hostname_b64(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_hostname_b64(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["hostname"], dsrc.metadata["local-hostname"] - ) + assert ret is True + assert MOCK_RETURNS["hostname"] == dsrc.metadata["local-hostname"] - def test_hostname(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_hostname(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["hostname"], dsrc.metadata["local-hostname"] - ) + assert ret is True + assert MOCK_RETURNS["hostname"] == dsrc.metadata["local-hostname"] - def test_hostname_if_no_sdc_hostname(self): + def test_hostname_if_no_sdc_hostname(self, ds, m_jmc_client_factory): my_returns = MOCK_RETURNS.copy() my_returns["sdc:hostname"] = "sdc-" + my_returns["hostname"] - dsrc = self._get_ds(mockdata=my_returns) + m_jmc_client_factory.return_value = PsuedoJoyentClient(my_returns) + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - my_returns["hostname"], dsrc.metadata["local-hostname"] - ) + assert ret is True + assert my_returns["hostname"] == dsrc.metadata["local-hostname"] - def test_sdc_hostname_if_no_hostname(self): + def test_sdc_hostname_if_no_hostname(self, ds, m_jmc_client_factory): my_returns = MOCK_RETURNS.copy() my_returns["sdc:hostname"] = "sdc-" + my_returns["hostname"] del my_returns["hostname"] - dsrc = self._get_ds(mockdata=my_returns) + m_jmc_client_factory.return_value = PsuedoJoyentClient(my_returns) + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - my_returns["sdc:hostname"], dsrc.metadata["local-hostname"] - ) + assert ret is True + assert my_returns["sdc:hostname"] == dsrc.metadata["local-hostname"] - def test_sdc_uuid_if_no_hostname_or_sdc_hostname(self): + def test_sdc_uuid_if_no_hostname_or_sdc_hostname( + self, ds, m_jmc_client_factory + ): my_returns = MOCK_RETURNS.copy() del my_returns["hostname"] - dsrc = self._get_ds(mockdata=my_returns) + m_jmc_client_factory.return_value = PsuedoJoyentClient(my_returns) + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - my_returns["sdc:uuid"], dsrc.metadata["local-hostname"] - ) + assert ret is True + assert my_returns["sdc:uuid"] == dsrc.metadata["local-hostname"] - def test_userdata(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_userdata(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["user-data"], dsrc.metadata["legacy-user-data"] - ) - self.assertEqual( - MOCK_RETURNS["cloud-init:user-data"], dsrc.userdata_raw - ) + assert ret is True + assert MOCK_RETURNS["user-data"] == dsrc.metadata["legacy-user-data"] + assert MOCK_RETURNS["cloud-init:user-data"] == dsrc.userdata_raw - def test_sdc_nics(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_sdc_nics(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - json.loads(MOCK_RETURNS["sdc:nics"]), dsrc.metadata["network-data"] + assert ret is True + assert ( + json.loads(MOCK_RETURNS["sdc:nics"]) + == dsrc.metadata["network-data"] ) - def test_sdc_scripts(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_sdc_scripts(self, ds, legacy_user_d, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["user-script"], dsrc.metadata["user-script"] - ) + assert ret is True + assert MOCK_RETURNS["user-script"] == dsrc.metadata["user-script"] - legacy_script_f = "%s/user-script" % self.legacy_user_d + legacy_script_f = "%s/user-script" % legacy_user_d print("legacy_script_f=%s" % legacy_script_f) - self.assertTrue(os.path.exists(legacy_script_f)) - self.assertTrue(os.path.islink(legacy_script_f)) + assert os.path.exists(legacy_script_f) + assert os.path.islink(legacy_script_f) user_script_perm = oct(os.stat(legacy_script_f)[stat.ST_MODE])[-3:] - self.assertEqual(user_script_perm, "700") + assert user_script_perm == "700" - def test_scripts_shebanged(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_scripts_shebanged(self, ds, legacy_user_d, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["user-script"], dsrc.metadata["user-script"] - ) + assert ret is True + assert MOCK_RETURNS["user-script"] == dsrc.metadata["user-script"] - legacy_script_f = "%s/user-script" % self.legacy_user_d - self.assertTrue(os.path.exists(legacy_script_f)) - self.assertTrue(os.path.islink(legacy_script_f)) + legacy_script_f = "%s/user-script" % legacy_user_d + assert os.path.exists(legacy_script_f) + assert os.path.islink(legacy_script_f) shebang = None with open(legacy_script_f, "r") as f: shebang = f.readlines()[0].strip() - self.assertEqual(shebang, "#!/bin/bash") + assert shebang == "#!/bin/bash" user_script_perm = oct(os.stat(legacy_script_f)[stat.ST_MODE])[-3:] - self.assertEqual(user_script_perm, "700") + assert user_script_perm == "700" - def test_scripts_shebang_not_added(self): + def test_scripts_shebang_not_added( + self, ds, legacy_user_d, m_jmc_client_factory + ): """ Test that the SmartOS requirement that plain text scripts are executable. This test makes sure that plain texts scripts @@ -605,22 +590,21 @@ def test_scripts_shebang_not_added(self): ["#!/usr/bin/perl", 'print("hi")', ""] ) - dsrc = self._get_ds(mockdata=my_returns) + m_jmc_client_factory.return_value = PsuedoJoyentClient(my_returns) + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - my_returns["user-script"], dsrc.metadata["user-script"] - ) + assert ret is True + assert my_returns["user-script"] == dsrc.metadata["user-script"] - legacy_script_f = "%s/user-script" % self.legacy_user_d - self.assertTrue(os.path.exists(legacy_script_f)) - self.assertTrue(os.path.islink(legacy_script_f)) + legacy_script_f = "%s/user-script" % legacy_user_d + assert os.path.exists(legacy_script_f) + assert os.path.islink(legacy_script_f) shebang = None with open(legacy_script_f, "r") as f: shebang = f.readlines()[0].strip() - self.assertEqual(shebang, "#!/usr/bin/perl") + assert shebang == "#!/usr/bin/perl" - def test_userdata_removed(self): + def test_userdata_removed(self, ds, legacy_user_d, m_jmc_client_factory): """ User-data in the SmartOS world is supposed to be written to a file each and every boot. This tests to make sure that in the event the @@ -628,83 +612,82 @@ def test_userdata_removed(self): and there is no /var/db/user-data left. """ - user_data_f = "%s/mdata-user-data" % self.legacy_user_d + user_data_f = "%s/mdata-user-data" % legacy_user_d with open(user_data_f, "w") as f: f.write("PREVIOUS") my_returns = MOCK_RETURNS.copy() del my_returns["user-data"] - dsrc = self._get_ds(mockdata=my_returns) + m_jmc_client_factory.return_value = PsuedoJoyentClient(my_returns) + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertFalse(dsrc.metadata.get("legacy-user-data")) + assert ret is True + assert dsrc.metadata.get("legacy-user-data") is None found_new = False - for root, _dirs, files in os.walk(self.legacy_user_d): + for root, _dirs, files in os.walk(legacy_user_d): for name in files: name_f = os.path.join(root, name) permissions = oct(os.stat(name_f)[stat.ST_MODE])[-3:] if re.match(r".*\/mdata-user-data$", name_f): found_new = True print(name_f) - self.assertEqual(permissions, "400") + assert permissions == "400" - self.assertFalse(found_new) + assert found_new is False - def test_vendor_data_not_default(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_vendor_data_not_default(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["sdc:vendor-data"], dsrc.metadata["vendor-data"] - ) + assert ret is True + assert MOCK_RETURNS["sdc:vendor-data"] == dsrc.metadata["vendor-data"] - def test_default_vendor_data(self): + def test_default_vendor_data(self, ds, m_jmc_client_factory): my_returns = MOCK_RETURNS.copy() def_op_script = my_returns["sdc:vendor-data"] del my_returns["sdc:vendor-data"] - dsrc = self._get_ds(mockdata=my_returns) + m_jmc_client_factory.return_value = PsuedoJoyentClient(my_returns) + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertNotEqual(def_op_script, dsrc.metadata["vendor-data"]) + assert ret is True + assert def_op_script != dsrc.metadata["vendor-data"] # we expect default vendor-data is a boothook - self.assertTrue(dsrc.vendordata_raw.startswith("#cloud-boothook")) + assert dsrc.vendordata_raw.startswith("#cloud-boothook") - def test_disable_iptables_flag(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_disable_iptables_flag(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["disable_iptables_flag"], - dsrc.metadata["iptables_disable"], + assert ret is True + assert ( + MOCK_RETURNS["disable_iptables_flag"] + == dsrc.metadata["iptables_disable"] ) - def test_motd_sys_info(self): - dsrc = self._get_ds(mockdata=MOCK_RETURNS) + def test_motd_sys_info(self, ds, m_jmc_client_factory): + dsrc = ds() ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - MOCK_RETURNS["enable_motd_sys_info"], - dsrc.metadata["motd_sys_info"], + assert ret is True + assert ( + MOCK_RETURNS["enable_motd_sys_info"] + == dsrc.metadata["motd_sys_info"] ) - def test_default_ephemeral(self): + def test_default_ephemeral(self, ds): # Test to make sure that the builtin config has the ephemeral # configuration. - dsrc = self._get_ds() + dsrc = ds() cfg = dsrc.get_config_obj() - ret = dsrc.get_data() - self.assertTrue(ret) + assert ret is True assert "disk_setup" in cfg assert "fs_setup" in cfg - self.assertIsInstance(cfg["disk_setup"], dict) - self.assertIsInstance(cfg["fs_setup"], list) + assert isinstance(cfg["disk_setup"], dict) + assert isinstance(cfg["fs_setup"], list) - def test_override_disk_aliases(self): + def test_override_disk_aliases(self, paths): # Test to make sure that the built-in DS is overriden builtin = DataSourceSmartOS.BUILTIN_DS_CONFIG @@ -712,55 +695,51 @@ def test_override_disk_aliases(self): # expect that these values are in builtin, or this is pointless for k in mydscfg: - self.assertIn(k, builtin) + assert k in builtin - dsrc = self._get_ds(ds_cfg=mydscfg) + dsrc = _get_ds(paths, ds_cfg=mydscfg) ret = dsrc.get_data() - self.assertTrue(ret) + assert ret is True - self.assertEqual( - mydscfg["disk_aliases"]["FOO"], dsrc.ds_cfg["disk_aliases"]["FOO"] + assert ( + mydscfg["disk_aliases"]["FOO"] + == dsrc.ds_cfg["disk_aliases"]["FOO"] ) - self.assertEqual( - dsrc.device_name_to_device("FOO"), mydscfg["disk_aliases"]["FOO"] + assert ( + dsrc.device_name_to_device("FOO") == mydscfg["disk_aliases"]["FOO"] ) - def test_reconfig_network_on_boot(self): + def test_reconfig_network_on_boot(self, ds, m_jmc_client_factory): # Test to ensure that network is configured from metadata on each boot - dsrc = self._get_ds(mockdata=MOCK_RETURNS) - self.assertSetEqual( - { - EventType.BOOT_NEW_INSTANCE, - EventType.BOOT, - EventType.BOOT_LEGACY, - }, - dsrc.default_update_events[EventScope.NETWORK], - ) + assert { + EventType.BOOT_NEW_INSTANCE, + EventType.BOOT, + EventType.BOOT_LEGACY, + } == ds().default_update_events[EventScope.NETWORK] -class TestIdentifyFile(CiTestCase): +class TestIdentifyFile: """Test the 'identify_file' utility.""" + @pytest.mark.allow_subp_for("file") @skipIf(not which("file"), "command 'file' not available.") - def test_file_happy_path(self): + def test_file_happy_path(self, tmp_path): """Test file is available and functional on plain text.""" - fname = self.tmp_path("myfile") + fname = str(tmp_path / "myfile") write_file(fname, "plain text content here\n") - with self.allow_subp(["file"]): - self.assertEqual("text/plain", identify_file(fname)) + assert "text/plain" == identify_file(fname) @mock.patch(DSMOS + ".subp.subp") - def test_returns_none_on_error(self, m_subp): + def test_returns_none_on_error(self, m_subp, tmp_path): """On 'file' execution error, None should be returned.""" m_subp.side_effect = ProcessExecutionError("FILE_FAILED", exit_code=99) - fname = self.tmp_path("myfile") + fname = str(tmp_path / "myfile") write_file(fname, "plain text content here\n") - self.assertEqual(None, identify_file(fname)) - self.assertEqual( - [mock.call(["file", "--brief", "--mime-type", fname])], - m_subp.call_args_list, - ) + assert None is identify_file(fname) + assert [ + mock.call(["file", "--brief", "--mime-type", fname]) + ] == m_subp.call_args_list class ShortReader: @@ -798,249 +777,300 @@ def read(self, size=-1): return ret -class TestJoyentMetadataClient(FilesystemMockingTestCase): +@pytest.fixture +def m_serial(mocker): + return mocker.MagicMock(spec=serial.Serial) - invalid = b"invalid command\n" - failure = b"FAILURE\n" - v2_ok = b"V2_OK\n" - - def setUp(self): - super(TestJoyentMetadataClient, self).setUp() - - self.serial = mock.MagicMock(spec=serial.Serial) - self.request_id = 0xABCDEF12 - self.metadata_value = "value" - self.response_parts = { - "command": "SUCCESS", - "crc": "b5a9ff00", - "length": SUCCESS_LEN + len(b64e(self.metadata_value)), - "payload": b64e(self.metadata_value), - "request_id": "{0:08x}".format(self.request_id), - } - def make_response(): - payloadstr = "" - if "payload" in self.response_parts: - payloadstr = " {0}".format(self.response_parts["payload"]) - return ( - "V2 {length} {crc} {request_id} " - "{command}{payloadstr}\n".format( - payloadstr=payloadstr, **self.response_parts - ).encode("ascii") - ) +@pytest.fixture +def joyent_metadata(mocker, m_serial): + res = namedtuple( + "joyent", + [ + "serial", + "request_id", + "metadata_value", + "response_parts", + "meta_source_data", + "metasource_data_len", + ], + defaults=(None, None, None, None, None, None), + ) + + res.serial = m_serial + res.request_id = 0xABCDEF12 + res.metadata_value = "value" + res.response_parts = { + "command": "SUCCESS", + "crc": "b5a9ff00", + "length": SUCCESS_LEN + len(b64e(res.metadata_value)), + "payload": b64e(res.metadata_value), + "request_id": "{0:08x}".format(res.request_id), + } - self.metasource_data = None - - def read_response(length): - if not self.metasource_data: - self.metasource_data = make_response() - self.metasource_data_len = len(self.metasource_data) - resp = self.metasource_data[:length] - self.metasource_data = self.metasource_data[length:] - return resp - - self.serial.read.side_effect = read_response - self.patched_funcs.enter_context( - mock.patch( - "cloudinit.sources.DataSourceSmartOS.random.randint", - mock.Mock(return_value=self.request_id), - ) + def make_response(): + payloadstr = "" + if "payload" in res.response_parts: + payloadstr = " {0}".format(res.response_parts["payload"]) + return ( + "V2 {length} {crc} {request_id} " + "{command}{payloadstr}\n".format( + payloadstr=payloadstr, **res.response_parts + ).encode("ascii") ) - def _get_client(self): - return DataSourceSmartOS.JoyentMetadataClient( - fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM - ) + res.metasource_data = None + + def read_response(length): + if not res.metasource_data: + res.metasource_data = make_response() + res.metasource_data_len = len(res.metasource_data) + resp = res.metasource_data[:length] + res.metasource_data = res.metasource_data[length:] + return resp + + res.serial.read.side_effect = read_response + mocker.patch( + "cloudinit.sources.DataSourceSmartOS.random.randint", + mock.Mock(return_value=res.request_id), + ) + return res + + +@pytest.fixture +def joyent_client(joyent_metadata): + return DataSourceSmartOS.JoyentMetadataClient( + fp=joyent_metadata.serial, + smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM, + ) + + +def _get_written_line(joyent_client, m_serial, key="some_key"): + joyent_client.get(key) + return m_serial.write.call_args[0][0] + + +@pytest.fixture +def joyent_serial_client(joyent_metadata): + joyent_metadata.serial.timeout = 1 + return DataSourceSmartOS.JoyentMetadataSerialClient( + None, fp=joyent_metadata.serial + ) - def _get_serial_client(self): - self.serial.timeout = 1 - return DataSourceSmartOS.JoyentMetadataSerialClient( - None, fp=self.serial - ) + +@pytest.mark.usefixtures("fake_filesystem") +class TestJoyentMetadataClient: + + invalid = b"invalid command\n" + failure = b"FAILURE\n" + v2_ok = b"V2_OK\n" def assertEndsWith(self, haystack, prefix): - self.assertTrue( - haystack.endswith(prefix), - "{0} does not end with '{1}'".format(repr(haystack), prefix), + assert haystack.endswith(prefix), "{0} does not end with '{1}'".format( + repr(haystack), prefix ) def assertStartsWith(self, haystack, prefix): - self.assertTrue( - haystack.startswith(prefix), - "{0} does not start with '{1}'".format(repr(haystack), prefix), - ) + assert haystack.startswith( + prefix + ), "{0} does not start with '{1}'".format(repr(haystack), prefix) def assertNoMoreSideEffects(self, obj): - self.assertRaises(StopIteration, obj) + with pytest.raises(StopIteration): + obj() - def test_get_metadata_writes_a_single_line(self): - client = self._get_client() - client.get("some_key") - self.assertEqual(1, self.serial.write.call_count) - written_line = self.serial.write.call_args[0][0] + def test_get_metadata_writes_a_single_line(self, m_serial, joyent_client): + joyent_client.get("some_key") + assert 1 == m_serial.write.call_count + written_line = m_serial.write.call_args[0][0] self.assertEndsWith( written_line.decode("ascii"), b"\n".decode("ascii") ) - self.assertEqual(1, written_line.count(b"\n")) - - def _get_written_line(self, key="some_key"): - client = self._get_client() - client.get(key) - return self.serial.write.call_args[0][0] + assert 1 == written_line.count(b"\n") - def test_get_metadata_writes_bytes(self): - self.assertIsInstance(self._get_written_line(), bytes) + def test_get_metadata_writes_bytes(self, joyent_client, m_serial): + assert isinstance(_get_written_line(joyent_client, m_serial), bytes) - def test_get_metadata_line_starts_with_v2(self): - foo = self._get_written_line() + def test_get_metadata_line_starts_with_v2(self, joyent_client, m_serial): + foo = _get_written_line(joyent_client, m_serial) self.assertStartsWith(foo.decode("ascii"), b"V2".decode("ascii")) - def test_get_metadata_uses_get_command(self): - parts = self._get_written_line().decode("ascii").strip().split(" ") - self.assertEqual("GET", parts[4]) + def test_get_metadata_uses_get_command(self, joyent_client, m_serial): + parts = ( + _get_written_line(joyent_client, m_serial) + .decode("ascii") + .strip() + .split(" ") + ) + assert "GET" == parts[4] - def test_get_metadata_base64_encodes_argument(self): + def test_get_metadata_base64_encodes_argument( + self, joyent_client, m_serial + ): key = "my_key" - parts = self._get_written_line(key).decode("ascii").strip().split(" ") - self.assertEqual(b64e(key), parts[5]) + parts = ( + _get_written_line(joyent_client, m_serial, key) + .decode("ascii") + .strip() + .split(" ") + ) + assert b64e(key) == parts[5] - def test_get_metadata_calculates_length_correctly(self): - parts = self._get_written_line().decode("ascii").strip().split(" ") + def test_get_metadata_calculates_length_correctly( + self, joyent_client, m_serial + ): + parts = ( + _get_written_line(joyent_client, m_serial) + .decode("ascii") + .strip() + .split(" ") + ) expected_length = len(" ".join(parts[3:])) - self.assertEqual(expected_length, int(parts[1])) + assert expected_length == int(parts[1]) - def test_get_metadata_uses_appropriate_request_id(self): - parts = self._get_written_line().decode("ascii").strip().split(" ") + def test_get_metadata_uses_appropriate_request_id( + self, joyent_client, m_serial + ): + parts = ( + _get_written_line(joyent_client, m_serial) + .decode("ascii") + .strip() + .split(" ") + ) request_id = parts[3] - self.assertEqual(8, len(request_id)) - self.assertEqual(request_id, request_id.lower()) + assert 8 == len(request_id) + assert request_id == request_id.lower() - def test_get_metadata_uses_random_number_for_request_id(self): - line = self._get_written_line() + def test_get_metadata_uses_random_number_for_request_id( + self, joyent_client, joyent_metadata, m_serial + ): + line = _get_written_line(joyent_client, m_serial) request_id = line.decode("ascii").strip().split(" ")[3] - self.assertEqual("{0:08x}".format(self.request_id), request_id) - - def test_get_metadata_checksums_correctly(self): - parts = self._get_written_line().decode("ascii").strip().split(" ") + assert "{0:08x}".format(joyent_metadata.request_id) == request_id + + def test_get_metadata_checksums_correctly(self, joyent_client, m_serial): + parts = ( + _get_written_line(joyent_client, m_serial) + .decode("ascii") + .strip() + .split(" ") + ) expected_checksum = "{0:08x}".format( crc32(" ".join(parts[3:]).encode("utf-8")) & 0xFFFFFFFF ) checksum = parts[2] - self.assertEqual(expected_checksum, checksum) - - def test_get_metadata_reads_a_line(self): - client = self._get_client() - client.get("some_key") - self.assertEqual(self.metasource_data_len, self.serial.read.call_count) - - def test_get_metadata_returns_valid_value(self): - client = self._get_client() - value = client.get("some_key") - self.assertEqual(self.metadata_value, value) - - def test_get_metadata_throws_exception_for_incorrect_length(self): - self.response_parts["length"] = 0 - client = self._get_client() - self.assertRaises( - DataSourceSmartOS.JoyentMetadataFetchException, - client.get, - "some_key", - ) + assert expected_checksum == checksum - def test_get_metadata_throws_exception_for_incorrect_crc(self): - self.response_parts["crc"] = "deadbeef" - client = self._get_client() - self.assertRaises( - DataSourceSmartOS.JoyentMetadataFetchException, - client.get, - "some_key", - ) + def test_get_metadata_reads_a_line( + self, joyent_client, joyent_metadata, m_serial + ): + joyent_client.get("some_key") + assert joyent_metadata.metasource_data_len == m_serial.read.call_count - def test_get_metadata_throws_exception_for_request_id_mismatch(self): - self.response_parts["request_id"] = "deadbeef" - client = self._get_client() - client._checksum = lambda _: self.response_parts["crc"] - self.assertRaises( - DataSourceSmartOS.JoyentMetadataFetchException, - client.get, - "some_key", - ) + def test_get_metadata_returns_valid_value( + self, joyent_client, joyent_metadata + ): + value = joyent_client.get("some_key") + assert joyent_metadata.metadata_value == value + + def test_get_metadata_throws_exception_for_incorrect_length( + self, joyent_client, joyent_metadata + ): + joyent_metadata.response_parts["length"] = 0 + with pytest.raises(DataSourceSmartOS.JoyentMetadataFetchException): + joyent_client.get( + "some_key", + ) - def test_get_metadata_returns_None_if_value_not_found(self): - self.response_parts["payload"] = "" - self.response_parts["command"] = "NOTFOUND" - self.response_parts["length"] = NOTFOUND_LEN - client = self._get_client() - client._checksum = lambda _: self.response_parts["crc"] - self.assertIsNone(client.get("some_key")) + def test_get_metadata_throws_exception_for_incorrect_crc( + self, joyent_client, joyent_metadata + ): + joyent_metadata.response_parts["crc"] = "deadbeef" + with pytest.raises(DataSourceSmartOS.JoyentMetadataFetchException): + joyent_client.get( + "some_key", + ) - def test_negotiate(self): - client = self._get_client() + def test_get_metadata_throws_exception_for_request_id_mismatch( + self, joyent_client, joyent_metadata + ): + joyent_metadata.response_parts["request_id"] = "deadbeef" + joyent_client._checksum = lambda _: joyent_metadata.response_parts[ + "crc" + ] + with pytest.raises(DataSourceSmartOS.JoyentMetadataFetchException): + joyent_client.get("some_key") + + def test_get_metadata_returns_None_if_value_not_found( + self, joyent_client, joyent_metadata + ): + joyent_metadata.response_parts["payload"] = "" + joyent_metadata.response_parts["command"] = "NOTFOUND" + joyent_metadata.response_parts["length"] = NOTFOUND_LEN + joyent_client._checksum = lambda _: joyent_metadata.response_parts[ + "crc" + ] + assert joyent_client.get("some_key") is None + + def test_negotiate(self, joyent_client): reader = ShortReader(self.v2_ok) - client.fp.read.side_effect = reader.read - client._negotiate() - self.assertTrue(reader.emptied) + joyent_client.fp.read.side_effect = reader.read + joyent_client._negotiate() + assert reader.emptied - def test_negotiate_short_response(self): - client = self._get_client() + def test_negotiate_short_response(self, joyent_client): # chopped '\n' from v2_ok. reader = ShortReader(self.v2_ok[:-1] + b"\0") - client.fp.read.side_effect = reader.read - self.assertRaises( - DataSourceSmartOS.JoyentMetadataTimeoutException, client._negotiate - ) - self.assertTrue(reader.emptied) + joyent_client.fp.read.side_effect = reader.read + with pytest.raises(DataSourceSmartOS.JoyentMetadataTimeoutException): + joyent_client._negotiate() + assert reader.emptied - def test_negotiate_bad_response(self): - client = self._get_client() + def test_negotiate_bad_response(self, joyent_client): reader = ShortReader(b"garbage\n" + self.v2_ok) - client.fp.read.side_effect = reader.read - self.assertRaises( - DataSourceSmartOS.JoyentMetadataFetchException, client._negotiate - ) - self.assertEqual(self.v2_ok, client.fp.read()) + joyent_client.fp.read.side_effect = reader.read + with pytest.raises(DataSourceSmartOS.JoyentMetadataFetchException): + joyent_client._negotiate() + assert self.v2_ok == joyent_client.fp.read() - def test_serial_open_transport(self): - client = self._get_serial_client() + def test_serial_open_transport(self, joyent_serial_client): reader = ShortReader(b"garbage\0" + self.invalid + self.v2_ok) - client.fp.read.side_effect = reader.read - client.open_transport() - self.assertTrue(reader.emptied) + joyent_serial_client.fp.read.side_effect = reader.read + joyent_serial_client.open_transport() + assert reader.emptied - def test_flush_failure(self): - client = self._get_serial_client() + def test_flush_failure(self, joyent_serial_client): reader = ShortReader( b"garbage" + b"\0" + self.failure + self.invalid + self.v2_ok ) - client.fp.read.side_effect = reader.read - client.open_transport() - self.assertTrue(reader.emptied) + joyent_serial_client.fp.read.side_effect = reader.read + joyent_serial_client.open_transport() + assert reader.emptied - def test_flush_many_timeouts(self): - client = self._get_serial_client() + def test_flush_many_timeouts(self, joyent_serial_client): reader = ShortReader(b"\0" * 100 + self.invalid + self.v2_ok) - client.fp.read.side_effect = reader.read - client.open_transport() - self.assertTrue(reader.emptied) + joyent_serial_client.fp.read.side_effect = reader.read + joyent_serial_client.open_transport() + assert reader.emptied - def test_list_metadata_returns_list(self): + def test_list_metadata_returns_list(self, joyent_client, joyent_metadata): parts = ["foo", "bar"] value = b64e("\n".join(parts)) - self.response_parts["payload"] = value - self.response_parts["crc"] = "40873553" - self.response_parts["length"] = SUCCESS_LEN + len(value) - client = self._get_client() - self.assertEqual(client.list(), parts) + joyent_metadata.response_parts["payload"] = value + joyent_metadata.response_parts["crc"] = "40873553" + joyent_metadata.response_parts["length"] = SUCCESS_LEN + len(value) + assert joyent_client.list() == parts - def test_list_metadata_returns_empty_list_if_no_customer_metadata(self): - del self.response_parts["payload"] - self.response_parts["length"] = SUCCESS_LEN - 1 - self.response_parts["crc"] = "14e563ba" - client = self._get_client() - self.assertEqual(client.list(), []) + def test_list_metadata_returns_empty_list_if_no_customer_metadata( + self, joyent_client, joyent_metadata + ): + del joyent_metadata.response_parts["payload"] + joyent_metadata.response_parts["length"] = SUCCESS_LEN - 1 + joyent_metadata.response_parts["crc"] = "14e563ba" + assert joyent_client.list() == [] -class TestNetworkConversion(CiTestCase): +class TestNetworkConversion: def test_convert_simple(self): expected = { "version": 1, @@ -1070,7 +1100,7 @@ def test_convert_simple(self): ], } found = convert_net(SDC_NICS) - self.assertEqual(expected, found) + assert expected == found def test_convert_simple_alt(self): expected = { @@ -1101,7 +1131,7 @@ def test_convert_simple_alt(self): ], } found = convert_net(SDC_NICS_ALT) - self.assertEqual(expected, found) + assert expected == found def test_convert_simple_dhcp(self): expected = { @@ -1130,7 +1160,7 @@ def test_convert_simple_dhcp(self): ], } found = convert_net(SDC_NICS_DHCP) - self.assertEqual(expected, found) + assert expected == found def test_convert_simple_multi_ip(self): expected = { @@ -1163,7 +1193,7 @@ def test_convert_simple_multi_ip(self): ], } found = convert_net(SDC_NICS_MIP) - self.assertEqual(expected, found) + assert expected == found def test_convert_with_dns(self): expected = { @@ -1201,7 +1231,7 @@ def test_convert_with_dns(self): dns_servers=["8.8.8.8", "8.8.8.1"], dns_domain="local", ) - self.assertEqual(expected, found) + assert expected == found def test_convert_simple_multi_ipv6(self): expected = { @@ -1238,7 +1268,7 @@ def test_convert_simple_multi_ipv6(self): ], } found = convert_net(SDC_NICS_MIP_IPV6) - self.assertEqual(expected, found) + assert expected == found def test_convert_simple_both_ipv4_ipv6(self): expected = { @@ -1276,7 +1306,7 @@ def test_convert_simple_both_ipv4_ipv6(self): ], } found = convert_net(SDC_NICS_IPV4_IPV6) - self.assertEqual(expected, found) + assert expected == found def test_gateways_not_on_all_nics(self): expected = { @@ -1307,7 +1337,7 @@ def test_gateways_not_on_all_nics(self): ], } found = convert_net(SDC_NICS_SINGLE_GATEWAY) - self.assertEqual(expected, found) + assert expected == found def test_routes_on_all_nics(self): routes = [ @@ -1366,7 +1396,7 @@ def test_routes_on_all_nics(self): } found = convert_net(SDC_NICS_SINGLE_GATEWAY, routes=routes) self.maxDiff = None - self.assertEqual(expected, found) + assert expected == found def test_ipv6_addrconf(self): expected = { @@ -1389,18 +1419,45 @@ def test_ipv6_addrconf(self): } found = convert_net(SDC_NICS_ADDRCONF) self.maxDiff = None - self.assertEqual(expected, found) + assert expected == found + + +@pytest.mark.allow_subp_for("mdata-get") +@pytest.fixture +def mdata_proc(): + mdata_proc = multiprocessing.Process(target=start_mdata_loop) + mdata_proc.start() + yield mdata_proc -@unittest.skipUnless( - get_smartos_environ() == SMARTOS_ENV_KVM, - "Only supported on KVM and bhyve guests under SmartOS", + # os.kill() rather than mdata_proc.terminate() to avoid console spam. + os.kill(mdata_proc.pid, signal.SIGKILL) + mdata_proc.join() + + +def start_mdata_loop(): + """ + The mdata-get command is repeatedly run in a separate process so + that it may try to race with metadata operations performed in the + main test process. Use of mdata-get is better than two processes + using the protocol implementation in DataSourceSmartOS because we + are testing to be sure that cloud-init and mdata-get respect each + others locks. + """ + rcs = list(range(256)) + while True: + subp(["mdata-get", "sdc:routes"], rcs=rcs) + + +@pytest.mark.skipif( + get_smartos_environ() != SMARTOS_ENV_KVM, + reason="Only supported on KVM and bhyve guests under SmartOS", ) -@unittest.skipUnless( - os.access(SERIAL_DEVICE, os.W_OK), - "Requires write access to " + SERIAL_DEVICE, +@pytest.mark.skipif( + not os.access(SERIAL_DEVICE, os.W_OK), + reason="Requires write access to " + SERIAL_DEVICE, ) -class TestSerialConcurrency(CiTestCase): +class TestSerialConcurrency: """ This class tests locking on an actual serial port, and as such can only be run in a kvm or bhyve guest running on a SmartOS host. A test run on @@ -1412,40 +1469,15 @@ class TestSerialConcurrency(CiTestCase): This takes on the order of 2 to 3 minutes to run. """ - allowed_subp = ["mdata-get"] - - def setUp(self): - self.mdata_proc = multiprocessing.Process(target=self.start_mdata_loop) - self.mdata_proc.start() - super(TestSerialConcurrency, self).setUp() - - def tearDown(self): - # os.kill() rather than mdata_proc.terminate() to avoid console spam. - os.kill(self.mdata_proc.pid, signal.SIGKILL) - self.mdata_proc.join() - super().tearDown() - - def start_mdata_loop(self): - """ - The mdata-get command is repeatedly run in a separate process so - that it may try to race with metadata operations performed in the - main test process. Use of mdata-get is better than two processes - using the protocol implementation in DataSourceSmartOS because we - are testing to be sure that cloud-init and mdata-get respect each - others locks. - """ - rcs = list(range(256)) - while True: - subp(["mdata-get", "sdc:routes"], rcs=rcs) - - def test_all_keys(self): - self.assertIsNotNone(self.mdata_proc.pid) + @pytest.mark.allow_subp_for("mdata-get") + def test_all_keys(self, mdata_proc): + assert mdata_proc.pid is not None ds = DataSourceSmartOS keys = [tup[0] for tup in ds.SMARTOS_ATTRIB_MAP.values()] keys.extend(ds.SMARTOS_ATTRIB_JSON.values()) client = ds.jmc_client_factory(smartos_type=SMARTOS_ENV_KVM) - self.assertIsNotNone(client) + assert client is not None # The behavior that we are testing for was observed mdata-get running # 10 times at roughly the same time as cloud-init fetched each key @@ -1457,4 +1489,4 @@ def test_all_keys(self): # thrown any exceptions. client.get(key) - self.assertIsNone(self.mdata_proc.exitcode) + assert mdata_proc.exitcode is None From fbbe53c47d20b65412b5109bcc6f741721b53b71 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 16 Jul 2025 08:40:42 -0600 Subject: [PATCH 022/150] doc: detailed examples of openstack config alternatives for non-x86 images (#6303) --- doc/rtd/reference/breaking_changes.rst | 59 ++++++++++++++++++++------ doc/rtd/spelling_word_list.txt | 1 + 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/doc/rtd/reference/breaking_changes.rst b/doc/rtd/reference/breaking_changes.rst index 7f85c9df32b..bafb33f497a 100644 --- a/doc/rtd/reference/breaking_changes.rst +++ b/doc/rtd/reference/breaking_changes.rst @@ -11,7 +11,7 @@ releases. many operating system vendors patch out breaking changes in cloud-init to ensure consistent behavior on their platform. -25.1.3 +25.1.4 ====== Strict datasource identity before network @@ -37,28 +37,59 @@ The most likely affected cloud platforms are AltCloud, Ec2 and OpenStack for non-x86 architectures where DMI data is not exposed by the kernel. If your non-x86 architecture or images no longer detect the proper datasource, -any of the following steps can ensure proper detection of cloud-init config: +cloud-init will remain disabled and perform no configuration operations during +boot. -- Provide kernel commandline containing ``ds=`` - which forces ds-identify to discover a specific datasource. -- Image creators: provide a config file part such as - :file:`/etc/cloud/cloud.cfg.d/*.cfg` containing the - case-sensitive ``datasource_list: [ ]`` to force cloud-init - to use a specific datasource without performing discovery. +Any of the following alternatives can ensure proper enablement of cloud-init +in non-x86 images without DMI-data: -For example, to force OpenStack discovery in cloud-init any of the following -approaches work: +- When launching VMs with the openstack command line client, provide + ``--config-drive true``: -- OpenStack: `attach a ConfigDrive`_ as an alternative config source -- Kernel command line containing ``ds=openstack`` -- Custom images provide :file:`/etc/cloud/cloud.cfg.d/91-set-datasource.cfg` - containing: +.. code-block:: shell-session + + $ openstack server create ... --config-drive true + +- On the openstack image command line, modify specific image metadata to + require config drive for the image: + +.. code-block:: shell-session + + $ openstack image set --property img_config_drive=mandatory + + +- OpenStack image creators can place a config file in the image at + :file:`/etc/cloud/cloud.cfg.d/91_openstack.cfg` to force + cloud-init to use OpenStack without DMI-based discovery. The file must + contain a single datasource as follows: .. code-block:: yaml datasource_list: [ OpenStack ] +- Charmed OpenStack Admins using glance-simplestreams-sync can default all + syncronized images to use config_drive: + +.. code-block:: shell-session + + $ juju config glance-simplestreams-sync custom-properties="img_config_drive=mandatory" + + +- OpenStack Nova admins can globally configure Nova to provide config drives + to all images by default in :file:`/etc/nova/nova.conf`: + +.. code-block:: toml + + [DEFAULT] + force_config_drive = true + +- Alternatively, providing + :ref:`kernel command line arguments` to a + virtual machine containing ``ds=openstack`` will force ds-identify to use the + specific datasource. + + 25.1 ==== diff --git a/doc/rtd/spelling_word_list.txt b/doc/rtd/spelling_word_list.txt index 4da0b890079..7cbf2973ab3 100644 --- a/doc/rtd/spelling_word_list.txt +++ b/doc/rtd/spelling_word_list.txt @@ -190,6 +190,7 @@ setup-keymap shellscript shortid sigonly +simplestreams sk sle sles From 2a249193950d66b7390e6dd40dbcc484ed70a37c Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 14 Jul 2025 16:19:46 +0200 Subject: [PATCH 023/150] test: expose tests.unittests.ditros._get_distro (#6290) Move it to tests.unitests.helpers, make it public and prepare for use in other tests cases. --- tests/unittests/distros/__init__.py | 18 ---------- tests/unittests/distros/test_aosc.py | 5 ++- tests/unittests/distros/test_arch.py | 5 ++- tests/unittests/distros/test_azurelinux.py | 4 +-- tests/unittests/distros/test_create_users.py | 5 ++- tests/unittests/distros/test_dragonflybsd.py | 7 ++-- tests/unittests/distros/test_freebsd.py | 7 ++-- tests/unittests/distros/test_gentoo.py | 7 ++-- tests/unittests/distros/test_init.py | 22 ++++++------- .../unittests/distros/test_manage_service.py | 11 +++---- tests/unittests/distros/test_mariner.py | 4 +-- tests/unittests/distros/test_netbsd.py | 6 ++-- tests/unittests/distros/test_openbsd.py | 7 ++-- tests/unittests/distros/test_photon.py | 5 ++- tests/unittests/distros/test_sles.py | 5 ++- tests/unittests/helpers.py | 18 +++++++++- tests/unittests/sources/test_wsl.py | 33 +++++++++---------- 17 files changed, 78 insertions(+), 91 deletions(-) diff --git a/tests/unittests/distros/__init__.py b/tests/unittests/distros/__init__.py index e66b9446a28..da6365a5941 100644 --- a/tests/unittests/distros/__init__.py +++ b/tests/unittests/distros/__init__.py @@ -1,19 +1 @@ # This file is part of cloud-init. See LICENSE file for license information. -import copy - -from cloudinit import distros, helpers, settings - - -def _get_distro(dtype, system_info=None): - """Return a Distro class of distro 'dtype'. - - cfg is format of CFG_BUILTIN['system_info']. - - example: _get_distro("debian") - """ - if system_info is None: - system_info = copy.deepcopy(settings.CFG_BUILTIN["system_info"]) - system_info["distro"] = dtype - paths = helpers.Paths(system_info["paths"]) - distro_cls = distros.fetch(dtype) - return distro_cls(dtype, system_info, paths) diff --git a/tests/unittests/distros/test_aosc.py b/tests/unittests/distros/test_aosc.py index e8a66b7aef2..6f4b0eb0eab 100644 --- a/tests/unittests/distros/test_aosc.py +++ b/tests/unittests/distros/test_aosc.py @@ -1,10 +1,9 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase, get_distro class TestAOSC(CiTestCase): def test_get_distro(self): - distro = _get_distro("aosc") + distro = get_distro("aosc") self.assertEqual(distro.osfamily, "aosc") diff --git a/tests/unittests/distros/test_arch.py b/tests/unittests/distros/test_arch.py index 5a8c5fe9156..d387bfd0682 100644 --- a/tests/unittests/distros/test_arch.py +++ b/tests/unittests/distros/test_arch.py @@ -1,13 +1,12 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import util -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase, get_distro class TestArch(CiTestCase): def test_get_distro(self): - distro = _get_distro("arch") + distro = get_distro("arch") hostname = "myhostname" hostfile = self.tmp_path("hostfile") distro._write_hostname(hostname, hostfile) diff --git a/tests/unittests/distros/test_azurelinux.py b/tests/unittests/distros/test_azurelinux.py index 03c895bc2a2..519a8931698 100644 --- a/tests/unittests/distros/test_azurelinux.py +++ b/tests/unittests/distros/test_azurelinux.py @@ -2,7 +2,7 @@ from tests.unittests.helpers import CiTestCase -from . import _get_distro +from ..helpers import get_distro SYSTEM_INFO = { "paths": { @@ -15,7 +15,7 @@ class TestAzurelinux(CiTestCase): with_logs = True - distro = _get_distro("azurelinux", SYSTEM_INFO) + distro = get_distro("azurelinux", SYSTEM_INFO) expected_log_line = "Rely on Azure Linux default network config" def test_network_renderer(self): diff --git a/tests/unittests/distros/test_create_users.py b/tests/unittests/distros/test_create_users.py index 819e2b9b006..69b844b6ea0 100644 --- a/tests/unittests/distros/test_create_users.py +++ b/tests/unittests/distros/test_create_users.py @@ -6,8 +6,7 @@ import pytest from cloudinit import distros, features, lifecycle, ssh_util -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import mock +from tests.unittests.helpers import get_distro, mock from tests.unittests.util import abstract_to_concrete USER = "foo_user" @@ -368,7 +367,7 @@ def test_avoid_unlock_preexisting_user_empty_password( mocker, tmpdir, ): - dist = _get_distro(distro_name) + dist = get_distro(distro_name) dist.shadow_fn = tmpdir.join(dist.shadow_fn).strpath dist.shadow_extrausers_fn = tmpdir.join( dist.shadow_extrausers_fn diff --git a/tests/unittests/distros/test_dragonflybsd.py b/tests/unittests/distros/test_dragonflybsd.py index 5419eeeafd4..7222aef792b 100644 --- a/tests/unittests/distros/test_dragonflybsd.py +++ b/tests/unittests/distros/test_dragonflybsd.py @@ -1,8 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import cloudinit.util -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import mock +from tests.unittests.helpers import get_distro, mock M_PATH = "cloudinit.distros." @@ -10,7 +9,7 @@ class TestDragonFlyBSD: @mock.patch(M_PATH + "subp.subp") def test_add_user(self, m_subp): - distro = _get_distro("dragonflybsd") + distro = get_distro("dragonflybsd") assert True is distro.add_user("me2", uid=1234, default=False) assert [ mock.call( @@ -29,7 +28,7 @@ def test_add_user(self, m_subp): ] == m_subp.call_args_list def test_unlock_passwd(self, caplog): - distro = _get_distro("dragonflybsd") + distro = get_distro("dragonflybsd") distro.unlock_passwd("me2") assert ( "Dragonfly BSD/FreeBSD password lock is not reversible, " diff --git a/tests/unittests/distros/test_freebsd.py b/tests/unittests/distros/test_freebsd.py index 50fb8e9ffc0..bda0f907d3b 100644 --- a/tests/unittests/distros/test_freebsd.py +++ b/tests/unittests/distros/test_freebsd.py @@ -3,8 +3,7 @@ import os from cloudinit.util import find_freebsd_part, get_path_dev_freebsd -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, get_distro, mock M_PATH = "cloudinit.distros.freebsd." @@ -12,7 +11,7 @@ class TestFreeBSD: @mock.patch(M_PATH + "subp.subp") def test_add_user(self, m_subp): - distro = _get_distro("freebsd") + distro = get_distro("freebsd") assert True is distro.add_user("me2", uid=1234, default=False) assert [ mock.call( @@ -38,7 +37,7 @@ def test_add_user(self, m_subp): ] == m_subp.call_args_list def test_unlock_passwd(self, caplog): - distro = _get_distro("freebsd") + distro = get_distro("freebsd") distro.unlock_passwd("me2") assert ( "Dragonfly BSD/FreeBSD password lock is not reversible, " diff --git a/tests/unittests/distros/test_gentoo.py b/tests/unittests/distros/test_gentoo.py index 979e6d82638..340a5cd4204 100644 --- a/tests/unittests/distros/test_gentoo.py +++ b/tests/unittests/distros/test_gentoo.py @@ -1,13 +1,12 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import atomic_helper, util -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, get_distro, mock class TestGentoo(CiTestCase): def test_write_hostname(self, whatever=False): - distro = _get_distro("gentoo") + distro = get_distro("gentoo") hostname = "myhostname" hostfile = self.tmp_path("hostfile") distro._write_hostname(hostname, hostfile) @@ -19,7 +18,7 @@ def test_write_hostname(self, whatever=False): ) def test_write_existing_hostname_with_comments(self, whatever=False): - distro = _get_distro("gentoo") + distro = get_distro("gentoo") hostname = "myhostname" contents = '#This is the hostname\nhostname="localhost"' hostfile = self.tmp_path("hostfile") diff --git a/tests/unittests/distros/test_init.py b/tests/unittests/distros/test_init.py index 7809899ff85..4319c35d007 100644 --- a/tests/unittests/distros/test_init.py +++ b/tests/unittests/distros/test_init.py @@ -14,7 +14,7 @@ PackageInstallerError, _get_package_mirror_info, ) -from tests.unittests.distros import _get_distro +from tests.unittests.helpers import get_distro # In newer versions of Python, these characters will be omitted instead # of substituted because of security concerns. @@ -293,7 +293,7 @@ def test_log_errors_with_updating_package_source( M_PATH + "snap.Snap.update_package_sources", side_effect=snap_error, ) - _get_distro("ubuntu").update_package_sources() + get_distro("ubuntu").update_package_sources() for log in expected_logs: assert log in caplog.text @@ -337,7 +337,7 @@ def test_run_available_package_managers( m_snap_update = mocker.patch( M_PATH + "snap.Snap.update_package_sources" ) - _get_distro("ubuntu").update_package_sources() + get_distro("ubuntu").update_package_sources() if not snap_available: m_snap_update.assert_not_called() else: @@ -381,12 +381,12 @@ def m_subp(self, mocker): def test_invalid_yaml(self, m_apt_install): """Test that an invalid YAML raises an exception.""" with pytest.raises(ValueError): - _get_distro("debian").install_packages([["invalid"]]) + get_distro("debian").install_packages([["invalid"]]) m_apt_install.assert_not_called() def test_unknown_package_manager(self, m_apt_install, caplog): """Test that an unknown package manager raises an exception.""" - _get_distro("debian").install_packages( + get_distro("debian").install_packages( [{"apt": ["pkg1"]}, "pkg2", {"invalid": ["pkg3"]}] ) assert ( @@ -400,7 +400,7 @@ def test_unknown_package_manager(self, m_apt_install, caplog): def test_non_default_package_manager(self, m_apt_install, m_snap_install): """Test success from package manager not supported by distro.""" - _get_distro("debian").install_packages( + get_distro("debian").install_packages( [{"apt": ["pkg1"]}, "pkg2", {"snap": ["pkg3"]}] ) apt_install_args = m_apt_install.call_args_list[0][0][0] @@ -422,7 +422,7 @@ def test_non_default_package_manager_fail( PackageInstallerError, match="Failed to install the following packages: {'pkg3'}", ): - _get_distro("debian").install_packages( + get_distro("debian").install_packages( [{"apt": ["pkg1"]}, "pkg2", {"snap": ["pkg3"]}] ) @@ -432,7 +432,7 @@ def test_default_and_specific_package_manager( self, m_apt_install, m_snap_install ): """Test success from package manager not supported by distro.""" - _get_distro("ubuntu").install_packages( + get_distro("ubuntu").install_packages( ["pkg1", ["pkg3", "ver3"], {"apt": [["pkg2", "ver2"]]}] ) apt_install_args = m_apt_install.call_args_list[0][0][0] @@ -451,7 +451,7 @@ def test_specific_package_manager_fail_doesnt_retry( return_value=["pkg1"], ) with pytest.raises(PackageInstallerError): - _get_distro("ubuntu").install_packages([{"apt": ["pkg1"]}]) + get_distro("ubuntu").install_packages([{"apt": ["pkg1"]}]) apt_install_args = m_apt_install.call_args_list[0][0][0] assert "pkg1" in apt_install_args m_snap_install.assert_not_called() @@ -463,7 +463,7 @@ def test_no_attempt_if_no_package_manager( mocker.patch(M_PATH + "apt.Apt.available", return_value=False) mocker.patch(M_PATH + "snap.Snap.available", return_value=False) with pytest.raises(PackageInstallerError): - _get_distro("ubuntu").install_packages( + get_distro("ubuntu").install_packages( ["pkg1", "pkg2", {"other": "pkg3"}] ) m_apt_install.assert_not_called() @@ -544,7 +544,7 @@ def test_failed( return_value=snap_failed, ) with pytest.raises(PackageInstallerError) as exc: - _get_distro(distro).install_packages(pkg_list) + get_distro(distro).install_packages(pkg_list) message = exc.value.args[0] assert "Failed to install the following packages" in message for pkg in total_failed: diff --git a/tests/unittests/distros/test_manage_service.py b/tests/unittests/distros/test_manage_service.py index d7637d388a5..7a721d94842 100644 --- a/tests/unittests/distros/test_manage_service.py +++ b/tests/unittests/distros/test_manage_service.py @@ -1,7 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, get_distro, mock from tests.unittests.util import MockDistro @@ -34,7 +33,7 @@ def test_manage_service_service_initcmd(self, m_subp, m_sysd): @mock.patch.object(MockDistro, "uses_systemd", return_value=False) @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_rcservice_initcmd(self, m_subp, m_sysd): - dist = _get_distro("alpine") + dist = get_distro("alpine") dist.init_cmd = ["rc-service", "--nocolor"] dist.manage_service("start", "myssh") m_subp.assert_called_with( @@ -45,7 +44,7 @@ def test_manage_service_rcservice_initcmd(self, m_subp, m_sysd): @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_alpine_rcupdate_cmd(self, m_subp): - dist = _get_distro("alpine") + dist = get_distro("alpine") dist.update_cmd = ["rc-update", "--nocolor"] dist.manage_service("enable", "myssh") m_subp.assert_called_with( @@ -54,7 +53,7 @@ def test_manage_service_alpine_rcupdate_cmd(self, m_subp): @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_rcctl_initcmd(self, m_subp): - dist = _get_distro("openbsd") + dist = get_distro("openbsd") dist.init_cmd = ["rcctl"] dist.manage_service("start", "myssh") m_subp.assert_called_with( @@ -63,7 +62,7 @@ def test_manage_service_rcctl_initcmd(self, m_subp): @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_fbsd_service_initcmd(self, m_subp): - dist = _get_distro("freebsd") + dist = get_distro("freebsd") dist.init_cmd = ["service"] dist.manage_service("enable", "myssh") m_subp.assert_called_with( diff --git a/tests/unittests/distros/test_mariner.py b/tests/unittests/distros/test_mariner.py index 57f8d4981c1..33243b64021 100644 --- a/tests/unittests/distros/test_mariner.py +++ b/tests/unittests/distros/test_mariner.py @@ -2,7 +2,7 @@ from tests.unittests.helpers import CiTestCase -from . import _get_distro +from ..helpers import get_distro SYSTEM_INFO = { "paths": { @@ -15,7 +15,7 @@ class TestMariner(CiTestCase): with_logs = True - distro = _get_distro("mariner", SYSTEM_INFO) + distro = get_distro("mariner", SYSTEM_INFO) expected_log_line = "Rely on MarinerOS default network config" def test_network_renderer(self): diff --git a/tests/unittests/distros/test_netbsd.py b/tests/unittests/distros/test_netbsd.py index c4cb9a55122..e5df07d53da 100644 --- a/tests/unittests/distros/test_netbsd.py +++ b/tests/unittests/distros/test_netbsd.py @@ -2,7 +2,7 @@ import pytest -from tests.unittests.distros import _get_distro +from tests.unittests.helpers import get_distro try: # Blowfish not available in < 3.7, so this has never worked. Ignore failure @@ -18,7 +18,7 @@ class TestNetBSD: @mock.patch(M_PATH + "subp.subp") def test_add_user(self, m_subp): - distro = _get_distro("netbsd") + distro = get_distro("netbsd") assert True is distro.add_user("me2", uid=1234, default=False) assert [ mock.call( @@ -28,7 +28,7 @@ def test_add_user(self, m_subp): @mock.patch(M_PATH + "subp.subp") def test_unlock_passwd(self, m_subp, caplog): - distro = _get_distro("netbsd") + distro = get_distro("netbsd") distro.unlock_passwd("me2") assert [ mock.call(["usermod", "-C", "no", "me2"]) diff --git a/tests/unittests/distros/test_openbsd.py b/tests/unittests/distros/test_openbsd.py index 2bab0d3bd14..78342c176c0 100644 --- a/tests/unittests/distros/test_openbsd.py +++ b/tests/unittests/distros/test_openbsd.py @@ -1,7 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import mock +from tests.unittests.helpers import get_distro, mock M_PATH = "cloudinit.distros.openbsd." @@ -9,7 +8,7 @@ class TestOpenBSD: @mock.patch(M_PATH + "subp.subp") def test_add_user(self, m_subp): - distro = _get_distro("openbsd") + distro = get_distro("openbsd") assert True is distro.add_user("me2", uid=1234, default=False) assert [ mock.call( @@ -18,7 +17,7 @@ def test_add_user(self, m_subp): ] == m_subp.call_args_list def test_unlock_passwd(self, caplog): - distro = _get_distro("openbsd") + distro = get_distro("openbsd") distro.unlock_passwd("me2") assert ( "OpenBSD password lock is not reversible, " diff --git a/tests/unittests/distros/test_photon.py b/tests/unittests/distros/test_photon.py index c6c679ceb66..15b92d96a21 100644 --- a/tests/unittests/distros/test_photon.py +++ b/tests/unittests/distros/test_photon.py @@ -1,8 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import util -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, get_distro, mock SYSTEM_INFO = { "paths": { @@ -15,7 +14,7 @@ class TestPhoton(CiTestCase): with_logs = True - distro = _get_distro("photon", SYSTEM_INFO) + distro = get_distro("photon", SYSTEM_INFO) expected_log_line = "Rely on PhotonOS default network config" def test_network_renderer(self): diff --git a/tests/unittests/distros/test_sles.py b/tests/unittests/distros/test_sles.py index 7732c380d8e..c2f953cdbce 100644 --- a/tests/unittests/distros/test_sles.py +++ b/tests/unittests/distros/test_sles.py @@ -1,10 +1,9 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase, get_distro class TestSLES(CiTestCase): def test_get_distro(self): - distro = _get_distro("sles") + distro = get_distro("sles") self.assertEqual(distro.osfamily, "suse") diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 81d15ea5e7d..9d16bdd9423 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -1,6 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. # pylint: disable=attribute-defined-outside-init +import copy import functools import io import logging @@ -20,7 +21,7 @@ import responses -from cloudinit import atomic_helper, subp, util +from cloudinit import atomic_helper, distros, helpers, settings, subp, util from cloudinit.config.schema import ( SchemaValidationError, validate_cloudconfig_schema, @@ -649,3 +650,18 @@ def does_not_raise(): """ yield + + +def get_distro(dtype, system_info=None): + """Return a Distro class of distro 'dtype'. + + cfg is format of CFG_BUILTIN['system_info']. + + example: get_distro("debian") + """ + if system_info is None: + system_info = copy.deepcopy(settings.CFG_BUILTIN["system_info"]) + system_info["distro"] = dtype + paths = helpers.Paths(system_info["paths"]) + distro_cls = distros.fetch(dtype) + return distro_cls(dtype, system_info, paths) diff --git a/tests/unittests/sources/test_wsl.py b/tests/unittests/sources/test_wsl.py index 63ca0d1ae5d..9c062706086 100644 --- a/tests/unittests/sources/test_wsl.py +++ b/tests/unittests/sources/test_wsl.py @@ -15,8 +15,7 @@ from cloudinit import util from cloudinit.sources import DataSourceWSL as wsl -from tests.unittests.distros import _get_distro -from tests.unittests.helpers import does_not_raise, mock +from tests.unittests.helpers import does_not_raise, get_distro, mock INSTANCE_NAME = "Noble-MLKit" GOOD_MOUNTS = { @@ -403,7 +402,7 @@ def test_metadata_id_default(self, tmpdir, paths): ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) ds.get_data() @@ -426,7 +425,7 @@ def test_metadata_id(self, tmpdir, paths): ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) ds.get_data() @@ -442,7 +441,7 @@ def test_get_data_cc(self, m_lsb_release, paths, tmpdir): ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -465,7 +464,7 @@ def test_get_data_sh(self, m_lsb_release, tmpdir, paths): data_path.write(f"#!/bin/sh\n{COMMAND}\n") ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -494,7 +493,7 @@ def test_get_data_jinja(self, m_lsb_release, paths, tmpdir): ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -530,7 +529,7 @@ def test_get_data_x( ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -579,7 +578,7 @@ def test_data_precedence(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -636,7 +635,7 @@ def test_interaction_with_pro(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -679,7 +678,7 @@ def test_landscape_vs_local_user(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -735,7 +734,7 @@ def test_landscape_provided_data(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -793,7 +792,7 @@ def test_landscape_empty_data(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -842,7 +841,7 @@ def test_landscape_shell_script(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -901,7 +900,7 @@ def test_with_landscape_no_tags(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -950,7 +949,7 @@ def test_with_no_tags_at_all(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) @@ -998,7 +997,7 @@ def test_with_no_client_subkey(self, m_get_linux_dist, tmpdir, paths): # Run the datasource ds = wsl.DataSourceWSL( sys_cfg=SAMPLE_CFG, - distro=_get_distro("ubuntu"), + distro=get_distro("ubuntu"), paths=paths, ) From 3a56dd0d6f38467c441614d5ae155fadef1c6828 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 14 Jul 2025 16:31:30 +0200 Subject: [PATCH 024/150] test: drop FilesystemMockingTestCase in netconfig (#6290) --- tests/unittests/distros/test_netconfig.py | 589 +++++++++++++--------- tests/unittests/helpers.py | 22 +- 2 files changed, 369 insertions(+), 242 deletions(-) diff --git a/tests/unittests/distros/test_netconfig.py b/tests/unittests/distros/test_netconfig.py index 54b387eadcd..8fba089dca1 100644 --- a/tests/unittests/distros/test_netconfig.py +++ b/tests/unittests/distros/test_netconfig.py @@ -3,20 +3,19 @@ import copy import os import re +import shutil from io import StringIO from textwrap import dedent from unittest import mock +import pytest import yaml -from cloudinit import distros, features, helpers, settings, subp, util +from cloudinit import features, subp, util from cloudinit.distros.parsers.sys_conf import SysConf from cloudinit.net.activators import IfUpDownActivator -from tests.unittests.helpers import ( - FilesystemMockingTestCase, - dir2dict, - readResource, -) +from tests.helpers import cloud_init_project_dir +from tests.unittests.helpers import dir2dict, get_distro BASE_NET_CFG = """ auto lo @@ -290,76 +289,77 @@ def __str__(self): return self.buffer.getvalue() -class TestNetCfgDistroBase(FilesystemMockingTestCase): - def setUp(self): - super(TestNetCfgDistroBase, self).setUp() - self.add_patch("cloudinit.util.system_is_snappy", "m_snappy") - - def _get_distro(self, dname, renderers=None, activators=None): - cls = distros.fetch(dname) - cfg = copy.deepcopy(settings.CFG_BUILTIN) - cfg["system_info"]["distro"] = dname - system_info_network_cfg = {} - if renderers: - system_info_network_cfg["renderers"] = renderers - if activators: - system_info_network_cfg["activators"] = activators - if system_info_network_cfg: - cfg["system_info"]["network"] = system_info_network_cfg - paths = helpers.Paths({}) - return cls(dname, cfg.get("system_info"), paths) - - def assertCfgEquals(self, blob1, blob2): - b1 = dict(SysConf(blob1.strip().splitlines())) - b2 = dict(SysConf(blob2.strip().splitlines())) - self.assertEqual(b1, b2) - for k, v in b1.items(): - self.assertIn(k, b2) - for k, v in b2.items(): - self.assertIn(k, b1) - for k, v in b1.items(): - self.assertEqual(v, b2[k]) - - -class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroFreeBSD, self).setUp() - ifs_txt = readResource("netinfo/freebsd-ifconfig-output") - with mock.patch( +@pytest.fixture(autouse=True) +def system_is_snappy(): + mock.patch("cloudinit.util.system_is_snappy") + + +def assertCfgEquals(blob1, blob2): + b1 = dict(SysConf(blob1.strip().splitlines())) + b2 = dict(SysConf(blob2.strip().splitlines())) + assert b1 == b2 + for k, v in b1.items(): + assert k in b2 + for k, v in b2.items(): + assert k in b1 + for k, v in b1.items(): + assert v == b2[k] + + +@pytest.fixture +def distro_freebsd(mocker): + with open("tests/data/netinfo/freebsd-ifconfig-output", "r") as fh: + ifs_txt = fh.read() + mocker.patch( "cloudinit.distros.networking.subp.subp", return_value=(ifs_txt, None), - ): - self.distro = self._get_distro("freebsd", renderers=["freebsd"]) + ) + return get_distro("freebsd", renderers=["freebsd"]) + + +@pytest.fixture(autouse=True) +def with_test_data(tmp_path, fake_filesystem_hook): + shutil.copytree( + str(cloud_init_project_dir("tests/data")), + str(tmp_path / "tests/data"), + dirs_exist_ok=True, + ) + +@pytest.mark.usefixtures("fake_filesystem") +class TestNetCfgDistroFreeBSD: def _apply_and_verify_freebsd( - self, apply_fn, config, expected_cfgs=None, bringup=False + self, apply_fn, config, tmp_path, expected_cfgs=None, bringup=False ): + rootd = tmp_path if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.freebsd.available") as m_avail: m_avail.return_value = True - with self.reRooted(tmpd) as tmpd: - util.ensure_dir("/etc") - util.ensure_file("/etc/rc.conf") - util.ensure_file("/etc/resolv.conf") - apply_fn(config, bringup) + util.ensure_dir(rootd / "etc") + util.ensure_file(rootd / "etc/rc.conf") + util.ensure_file(rootd / "etc/resolv.conf") + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(rootd), filter=lambda fn: fn.startswith(str(rootd / "etc")) + ) for cfgpath, expected in expected_cfgs.items(): print("----------") print(expected) print("^^^^ expected | rendered VVVVVVV") print(results[cfgpath]) print("----------") - self.assertEqual( - set(expected.split("\n")), set(results[cfgpath].split("\n")) + assert set(expected.split("\n")) == set( + results[cfgpath].split("\n") ) - self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + assert 0o644 == get_mode(cfgpath, str(rootd)) @mock.patch("cloudinit.net.get_interfaces_by_mac") - def test_apply_network_config_freebsd_standard(self, ifaces_mac): + def test_apply_network_config_freebsd_standard( + self, ifaces_mac, distro_freebsd, tmp_path + ): ifaces_mac.return_value = { "00:15:5d:4c:73:00": "eth0", } @@ -374,13 +374,16 @@ def test_apply_network_config_freebsd_standard(self, ifaces_mac): "/etc/resolv.conf": "", } self._apply_and_verify_freebsd( - self.distro.apply_network_config, + distro_freebsd.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) @mock.patch("cloudinit.net.get_interfaces_by_mac") - def test_apply_network_config_freebsd_ipv6_standard(self, ifaces_mac): + def test_apply_network_config_freebsd_ipv6_standard( + self, ifaces_mac, distro_freebsd, tmp_path + ): ifaces_mac.return_value = { "00:15:5d:4c:73:00": "eth0", } @@ -395,13 +398,16 @@ def test_apply_network_config_freebsd_ipv6_standard(self, ifaces_mac): "/etc/resolv.conf": "", } self._apply_and_verify_freebsd( - self.distro.apply_network_config, + distro_freebsd.apply_network_config, V1_NET_CFG_IPV6, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) @mock.patch("cloudinit.net.get_interfaces_by_mac") - def test_apply_network_config_freebsd_ifrename(self, ifaces_mac): + def test_apply_network_config_freebsd_ifrename( + self, ifaces_mac, distro_freebsd, tmp_path + ): ifaces_mac.return_value = { "00:15:5d:4c:73:00": "vtnet0", } @@ -420,13 +426,16 @@ def test_apply_network_config_freebsd_ifrename(self, ifaces_mac): "/etc/resolv.conf": "", } self._apply_and_verify_freebsd( - self.distro.apply_network_config, + distro_freebsd.apply_network_config, V1_NET_CFG_RENAME, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) @mock.patch("cloudinit.net.get_interfaces_by_mac") - def test_apply_network_config_freebsd_nameserver(self, ifaces_mac): + def test_apply_network_config_freebsd_nameserver( + self, ifaces_mac, distro_freebsd, tmp_path + ): ifaces_mac.return_value = { "00:15:5d:4c:73:00": "eth0", } @@ -436,19 +445,25 @@ def test_apply_network_config_freebsd_nameserver(self, ifaces_mac): V1_NET_CFG_DNS["config"][0]["subnets"][0]["dns_nameservers"] = ns expected_cfgs = {"/etc/resolv.conf": "nameserver 1.2.3.4\n"} self._apply_and_verify_freebsd( - self.distro.apply_network_config, + distro_freebsd.apply_network_config, V1_NET_CFG_DNS, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) -class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroUbuntuEni, self).setUp() - self.distro = self._get_distro( - "ubuntu", renderers=["eni"], activators=["eni"] - ) +@pytest.fixture +def distro_eni(): + return get_distro("ubuntu", renderers=["eni"], activators=["eni"]) + + +@pytest.fixture +def m_activators_subp(mocker): + mocker.patch("cloudinit.net.activators.subp.subp", return_value=("", "")) + +@pytest.mark.usefixtures("fake_filesystem", "m_activators_subp") +class TestNetCfgDistroUbuntuEni: def eni_path(self): return "/etc/network/interfaces.d/50-cloud-init.cfg" @@ -459,6 +474,7 @@ def _apply_and_verify_eni( self, apply_fn, config, + tmp_path, expected_cfgs=None, bringup=False, previous_files=(), @@ -466,106 +482,122 @@ def _apply_and_verify_eni( if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.eni.available") as m_avail: m_avail.return_value = True path_modes = {} - with self.reRooted(tmpd) as tmpd: - for previous_path, content, mode in previous_files: - util.write_file(previous_path, content, mode=mode) - path_modes[previous_path] = mode - apply_fn(config, bringup) - - results = dir2dict(tmpd) + for previous_path, content, mode in previous_files: + util.write_file(previous_path, content, mode=mode) + path_modes[previous_path] = mode + apply_fn(config, bringup) + + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected in expected_cfgs.items(): print("----------") print(expected) print("^^^^ expected | rendered VVVVVVV") print(results[cfgpath]) print("----------") - self.assertEqual(expected, results[cfgpath]) - self.assertEqual( - path_modes.get(cfgpath, 0o644), get_mode(cfgpath, tmpd) + assert expected == results[cfgpath] + assert path_modes.get(cfgpath, 0o644) == get_mode( + cfgpath, str(tmp_path) ) - def test_apply_network_config_and_bringup_filters_priority_eni_ub(self): + def test_apply_network_config_and_bringup_filters_priority_eni_ub( + self, distro_eni, tmp_path + ): """Network activator search priority can be overridden from config.""" expected_cfgs = { self.eni_path(): V1_NET_CFG_OUTPUT, } + with mock.patch( "cloudinit.net.activators.select_activator" ) as select_activator: select_activator.return_value = IfUpDownActivator self._apply_and_verify_eni( - self.distro.apply_network_config, + distro_eni.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), bringup=True, ) # 2nd call to select_activator via distro.network_activator prop - assert IfUpDownActivator == self.distro.network_activator - self.assertEqual( - [mock.call(priority=["eni"])] * 2, select_activator.call_args_list - ) + assert IfUpDownActivator == distro_eni.network_activator + assert [ + mock.call(priority=["eni"]) + ] * 2 == select_activator.call_args_list - def test_apply_network_config_and_bringup_activator_defaults_ub(self): + def test_apply_network_config_and_bringup_activator_defaults_ub( + self, tmp_path + ): """Network activator search priority defaults when unspecified.""" expected_cfgs = { self.eni_path(): V1_NET_CFG_OUTPUT, } # Don't set activators to see DEFAULT_PRIORITY - self.distro = self._get_distro("ubuntu", renderers=["eni"]) + distro = get_distro("ubuntu", renderers=["eni"]) with mock.patch( "cloudinit.net.activators.select_activator" ) as select_activator: select_activator.return_value = IfUpDownActivator self._apply_and_verify_eni( - self.distro.apply_network_config, + distro.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), bringup=True, ) # 2nd call to select_activator via distro.network_activator prop - assert IfUpDownActivator == self.distro.network_activator - self.assertEqual( - [mock.call(priority=None)] * 2, select_activator.call_args_list - ) + assert IfUpDownActivator == distro.network_activator + assert [ + mock.call(priority=None) + ] * 2 == select_activator.call_args_list - def test_apply_network_config_eni_ub(self): + def test_apply_network_config_eni_ub(self, distro_eni, tmp_path): expected_cfgs = { self.eni_path(): V1_NET_CFG_OUTPUT, self.rules_path(): "", } self._apply_and_verify_eni( - self.distro.apply_network_config, + distro_eni.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), previous_files=((self.rules_path(), "something", 0o660),), ) - def test_apply_network_config_ipv6_ub(self): + def test_apply_network_config_ipv6_ub(self, distro_eni, tmp_path): expected_cfgs = {self.eni_path(): V1_NET_CFG_IPV6_OUTPUT} self._apply_and_verify_eni( - self.distro.apply_network_config, + distro_eni.apply_network_config, V1_NET_CFG_IPV6, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) -class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase): +@pytest.fixture +def distro_netplan(): + return get_distro("ubuntu", renderers=["netplan"]) + + +@pytest.fixture +def m_netplan_subp(mocker): + mocker.patch("cloudinit.net.netplan.subp.subp", return_value=("", "")) - with_logs = True - def setUp(self): - super(TestNetCfgDistroUbuntuNetplan, self).setUp() - self.distro = self._get_distro("ubuntu", renderers=["netplan"]) - self.devlist = ["eth0", "lo"] +@pytest.mark.usefixtures("fake_filesystem", "m_netplan_subp") +class TestNetCfgDistroUbuntuNetplan: + DEV_LIST = ["eth0", "lo"] def _apply_and_verify_netplan( self, apply_fn, config, + tmp_path, expected_cfgs=None, bringup=False, previous_files=(), @@ -573,63 +605,75 @@ def _apply_and_verify_netplan( if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.netplan.available", return_value=True): with mock.patch( "cloudinit.net.netplan.get_devicelist", - return_value=self.devlist, + return_value=self.DEV_LIST, ): - with self.reRooted(tmpd) as tmpd: - for previous_path, content, mode in previous_files: - util.write_file(previous_path, content, mode=mode) - apply_fn(config, bringup) + for previous_path, content, mode in previous_files: + util.write_file(previous_path, content, mode=mode) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected, mode in expected_cfgs: print("----------") print(expected) print("^^^^ expected | rendered VVVVVVV") print(results[cfgpath]) print("----------") - self.assertEqual(expected, results[cfgpath]) - self.assertEqual(mode, get_mode(cfgpath, tmpd)) + assert expected == results[cfgpath] + assert mode == get_mode(cfgpath, str(tmp_path)) def netplan_path(self): return "/etc/netplan/50-cloud-init.yaml" - def test_apply_network_config_v1_to_netplan_ub(self): + def test_apply_network_config_v1_to_netplan_ub( + self, distro_netplan, tmp_path + ): expected_cfgs = ( (self.netplan_path(), V1_TO_V2_NET_CFG_OUTPUT, 0o600), ) self._apply_and_verify_netplan( - self.distro.apply_network_config, + distro_netplan.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs, ) - def test_apply_network_config_v1_ipv6_to_netplan_ub(self): + def test_apply_network_config_v1_ipv6_to_netplan_ub( + self, distro_netplan, tmp_path + ): expected_cfgs = ( (self.netplan_path(), V1_TO_V2_NET_CFG_IPV6_OUTPUT, 0o600), ) self._apply_and_verify_netplan( - self.distro.apply_network_config, + distro_netplan.apply_network_config, V1_NET_CFG_IPV6, + tmp_path, expected_cfgs=expected_cfgs, ) - def test_apply_network_config_v2_passthrough_ub(self): + def test_apply_network_config_v2_passthrough_ub( + self, distro_netplan, tmp_path + ): expected_cfgs = ( (self.netplan_path(), V2_TO_V2_NET_CFG_OUTPUT, 0o600), ) self._apply_and_verify_netplan( - self.distro.apply_network_config, + distro_netplan.apply_network_config, V2_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs, ) - def test_apply_network_config_v2_passthrough_retain_orig_perms(self): + def test_apply_network_config_v2_passthrough_retain_orig_perms( + self, distro_netplan, tmp_path + ): """Custom permissions on existing netplan is kept when more strict.""" expected_cfgs = ( (self.netplan_path(), V2_TO_V2_NET_CFG_OUTPUT, 0o640), @@ -641,15 +685,18 @@ def test_apply_network_config_v2_passthrough_retain_orig_perms(self): # we keep 640 because it's more strict. # 1640 is used to assert sticky bit preserved across write self._apply_and_verify_netplan( - self.distro.apply_network_config, + distro_netplan.apply_network_config, V2_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs, previous_files=( ("/etc/netplan/50-cloud-init.yaml", "a", 0o640), ), ) - def test_apply_network_config_v2_passthrough_ub_old_behavior(self): + def test_apply_network_config_v2_passthrough_ub_old_behavior( + self, distro_netplan, tmp_path + ): """Kinetic and earlier have 50-cloud-init.yaml world-readable""" expected_cfgs = ( (self.netplan_path(), V2_TO_V2_NET_CFG_OUTPUT, 0o644), @@ -658,31 +705,38 @@ def test_apply_network_config_v2_passthrough_ub_old_behavior(self): features, "NETPLAN_CONFIG_ROOT_READ_ONLY", False ): self._apply_and_verify_netplan( - self.distro.apply_network_config, + distro_netplan.apply_network_config, V2_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs, ) - def test_apply_network_config_v2_full_passthrough_ub(self): + def test_apply_network_config_v2_full_passthrough_ub( + self, caplog, distro_netplan, tmp_path + ): expected_cfgs = ( (self.netplan_path(), V2_PASSTHROUGH_NET_CFG_OUTPUT, 0o600), ) self._apply_and_verify_netplan( - self.distro.apply_network_config, + distro_netplan.apply_network_config, V2_PASSTHROUGH_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs, ) - self.assertIn("Passthrough netplan v2 config", self.logs.getvalue()) - self.assertIn( - "Selected renderer 'netplan' from priority list: ['netplan']", - self.logs.getvalue(), + assert "Passthrough netplan v2 config" in caplog.text + assert ( + "Selected renderer 'netplan' from priority list: ['netplan']" + in caplog.text ) -class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroRedhat, self).setUp() - self.distro = self._get_distro("rhel", renderers=["sysconfig"]) +@pytest.fixture +def distro_redhat(): + return get_distro("rhel", renderers=["sysconfig"]) + + +@pytest.mark.usefixtures("fake_filesystem") +class TestNetCfgDistroRedhat: def ifcfg_path(self, ifname): return "/etc/sysconfig/network-scripts/ifcfg-%s" % ifname @@ -694,24 +748,26 @@ def _apply_and_verify( self, apply_fn, config, + tmp_path, expected_cfgs=None, bringup=False, - tmpd=None, ): if not expected_cfgs: raise ValueError("expected_cfg must not be None") with mock.patch("cloudinit.net.sysconfig.available") as m_avail: m_avail.return_value = True - with self.reRooted(tmpd) as tmpd: - apply_fn(config, bringup) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected in expected_cfgs.items(): - self.assertCfgEquals(expected, results[cfgpath]) - self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + assertCfgEquals(expected, results[cfgpath]) + assert 0o644 == get_mode(cfgpath, str(tmp_path)) - def test_apply_network_config_rh(self): + def test_apply_network_config_rh(self, distro_redhat, tmp_path): expected_cfgs = { self.ifcfg_path("eth0"): dedent( """\ @@ -743,12 +799,13 @@ def test_apply_network_config_rh(self): } # rh_distro.apply_network_config(V1_NET_CFG, False) self._apply_and_verify( - self.distro.apply_network_config, + distro_redhat.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) - def test_apply_network_config_ipv6_rh(self): + def test_apply_network_config_ipv6_rh(self, distro_redhat, tmp_path): expected_cfgs = { self.ifcfg_path("eth0"): dedent( """\ @@ -784,12 +841,15 @@ def test_apply_network_config_ipv6_rh(self): } # rh_distro.apply_network_config(V1_NET_CFG_IPV6, False) self._apply_and_verify( - self.distro.apply_network_config, + distro_redhat.apply_network_config, V1_NET_CFG_IPV6, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) - def test_sysconfig_network_no_overwite_ipv6_rh(self): + def test_sysconfig_network_no_overwite_ipv6_rh( + self, distro_redhat, tmp_path + ): expected_cfgs = { self.ifcfg_path("eth0"): dedent( """\ @@ -824,24 +884,22 @@ def test_sysconfig_network_no_overwite_ipv6_rh(self): """ ), } - tmpdir = self.tmp_dir() file_mode = 0o644 # pre-existing config in /etc/sysconfig/network should not be removed - with self.reRooted(tmpdir) as tmpdir: - util.write_file( - self.control_path(), - "".join("NOZEROCONF=yes") + "\n", - file_mode, - ) + util.write_file( + self.control_path(), + "".join("NOZEROCONF=yes") + "\n", + file_mode, + ) self._apply_and_verify( - self.distro.apply_network_config, + distro_redhat.apply_network_config, V1_NET_CFG_IPV6, + tmp_path, expected_cfgs=expected_cfgs.copy(), - tmpd=tmpdir, ) - def test_vlan_render_unsupported(self): + def test_vlan_render_unsupported(self, distro_redhat, tmp_path): """Render officially unsupported vlan names.""" cfg = { "version": 2, @@ -891,10 +949,13 @@ def test_vlan_render_unsupported(self): ), } self._apply_and_verify( - self.distro.apply_network_config, cfg, expected_cfgs=expected_cfgs + distro_redhat.apply_network_config, + cfg, + tmp_path, + expected_cfgs=expected_cfgs, ) - def test_vlan_render(self): + def test_vlan_render(self, distro_redhat, tmp_path): cfg = { "version": 2, "ethernets": {"eth0": {"addresses": ["192.10.1.2/24"]}}, @@ -937,36 +998,42 @@ def test_vlan_render(self): ), } self._apply_and_verify( - self.distro.apply_network_config, cfg, expected_cfgs=expected_cfgs + distro_redhat.apply_network_config, + cfg, + tmp_path, + expected_cfgs=expected_cfgs, ) -class TestNetCfgDistroOpensuse(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroOpensuse, self).setUp() - self.distro = self._get_distro("opensuse", renderers=["sysconfig"]) +@pytest.fixture +def distro_opensuse(): + return get_distro("opensuse", renderers=["sysconfig"]) + +@pytest.mark.usefixtures("fake_filesystem") +class TestNetCfgDistroOpensuse: def ifcfg_path(self, ifname): return "/etc/sysconfig/network/ifcfg-%s" % ifname def _apply_and_verify( - self, apply_fn, config, expected_cfgs=None, bringup=False + self, apply_fn, config, tmp_path, expected_cfgs=None, bringup=False ): if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.sysconfig.available") as m_avail: m_avail.return_value = True - with self.reRooted(tmpd) as tmpd: - apply_fn(config, bringup) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected in expected_cfgs.items(): - self.assertCfgEquals(expected, results[cfgpath]) - self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + assertCfgEquals(expected, results[cfgpath]) + assert 0o644 == get_mode(cfgpath, str(tmp_path)) - def test_apply_network_config_opensuse(self): + def test_apply_network_config_opensuse(self, distro_opensuse, tmp_path): """Opensuse uses apply_network_config and renders sysconfig""" expected_cfgs = { self.ifcfg_path("eth0"): dedent( @@ -985,12 +1052,15 @@ def test_apply_network_config_opensuse(self): ), } self._apply_and_verify( - self.distro.apply_network_config, + distro_opensuse.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) - def test_apply_network_config_ipv6_opensuse(self): + def test_apply_network_config_ipv6_opensuse( + self, distro_opensuse, tmp_path + ): """Opensuse uses apply_network_config and renders sysconfig w/ipv6""" expected_cfgs = { self.ifcfg_path("eth0"): dedent( @@ -1008,21 +1078,25 @@ def test_apply_network_config_ipv6_opensuse(self): ), } self._apply_and_verify( - self.distro.apply_network_config, + distro_opensuse.apply_network_config, V1_NET_CFG_IPV6, + tmp_path, expected_cfgs=expected_cfgs.copy(), ) -class TestNetCfgDistroArch(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroArch, self).setUp() - self.distro = self._get_distro("arch", renderers=["netplan"]) +@pytest.fixture +def distro_arch(): + return get_distro("arch", renderers=["netplan"]) + +@pytest.mark.usefixtures("fake_filesystem", "m_netplan_subp") +class TestNetCfgDistroArch: def _apply_and_verify( self, apply_fn, config, + tmp_path, expected_cfgs=None, bringup=False, with_netplan=False, @@ -1030,14 +1104,15 @@ def _apply_and_verify( if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch( "cloudinit.net.netplan.available", return_value=with_netplan ): - with self.reRooted(tmpd) as tmpd: - apply_fn(config, bringup) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) mode = 0o600 if with_netplan else 0o644 for cfgpath, expected in expected_cfgs.items(): print("----------") @@ -1045,8 +1120,8 @@ def _apply_and_verify( print("^^^^ expected | rendered VVVVVVV") print(results[cfgpath]) print("----------") - self.assertEqual(expected, results[cfgpath]) - self.assertEqual(mode, get_mode(cfgpath, tmpd)) + assert expected == results[cfgpath] + assert mode == get_mode(cfgpath, str(tmp_path)) def netctl_path(self, iface): return "/etc/netctl/%s" % iface @@ -1054,7 +1129,7 @@ def netctl_path(self, iface): def netplan_path(self): return "/etc/netplan/50-cloud-init.yaml" - def test_apply_network_config_v1_with_netplan(self): + def test_apply_network_config_v1_with_netplan(self, distro_arch, tmp_path): expected_cfgs = { self.netplan_path(): dedent( """\ @@ -1078,18 +1153,22 @@ def test_apply_network_config_v1_with_netplan(self): "cloudinit.net.netplan.get_devicelist", return_value=[] ): self._apply_and_verify( - self.distro.apply_network_config, + distro_arch.apply_network_config, V1_NET_CFG, + tmp_path, expected_cfgs=expected_cfgs.copy(), with_netplan=True, ) -class TestNetCfgDistroPhoton(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroPhoton, self).setUp() - self.distro = self._get_distro("photon", renderers=["networkd"]) +@pytest.fixture +def distro_photon(mocker): + mocker.patch("cloudinit.net.networkd.util.chownbyname") + return get_distro("photon", renderers=["networkd"]) + +@pytest.mark.usefixtures("fake_filesystem") +class TestNetCfgDistroPhoton: def create_conf_dict(self, contents): content_dict = {} for line in contents: @@ -1106,25 +1185,26 @@ def create_conf_dict(self, contents): def compare_dicts(self, actual, expected): for k, v in actual.items(): - self.assertEqual(sorted(expected[k]), sorted(v)) + assert sorted(expected[k]) == sorted(v) def _apply_and_verify( - self, apply_fn, config, expected_cfgs=None, bringup=False + self, apply_fn, config, tmp_path, expected_cfgs=None, bringup=False ): if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.networkd.available") as m_avail: m_avail.return_value = True - with self.reRooted(tmpd) as tmpd: - apply_fn(config, bringup) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected in expected_cfgs.items(): actual = self.create_conf_dict(results[cfgpath].splitlines()) self.compare_dicts(actual, expected) - self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + assert 0o644 == get_mode(cfgpath, str(tmp_path)) def nwk_file_path(self, ifname): return "/etc/systemd/network/10-cloud-init-%s.network" % ifname @@ -1155,7 +1235,7 @@ def net_cfg_2(self, ifname): ) return ret - def test_photon_network_config_v1(self): + def test_photon_network_config_v1(self, distro_photon, tmp_path): tmp = self.net_cfg_1("eth0").splitlines() expected_eth0 = self.create_conf_dict(tmp) @@ -1168,10 +1248,13 @@ def test_photon_network_config_v1(self): } self._apply_and_verify( - self.distro.apply_network_config, V1_NET_CFG, expected_cfgs.copy() + distro_photon.apply_network_config, + V1_NET_CFG, + tmp_path, + expected_cfgs.copy(), ) - def test_photon_network_config_v2(self): + def test_photon_network_config_v2(self, distro_photon, tmp_path): tmp = self.net_cfg_1("eth7").splitlines() expected_eth7 = self.create_conf_dict(tmp) @@ -1184,10 +1267,15 @@ def test_photon_network_config_v2(self): } self._apply_and_verify( - self.distro.apply_network_config, V2_NET_CFG, expected_cfgs.copy() + distro_photon.apply_network_config, + V2_NET_CFG, + tmp_path, + expected_cfgs.copy(), ) - def test_photon_network_config_v1_with_duplicates(self): + def test_photon_network_config_v1_with_duplicates( + self, distro_photon, tmp_path + ): expected = """\ [Match] Name=eth0 @@ -1206,15 +1294,21 @@ def test_photon_network_config_v1_with_duplicates(self): } self._apply_and_verify( - self.distro.apply_network_config, net_cfg, expected_cfgs.copy() + distro_photon.apply_network_config, + net_cfg, + tmp_path, + expected_cfgs.copy(), ) -class TestNetCfgDistroMariner(TestNetCfgDistroBase): - def setUp(self): - super(TestNetCfgDistroMariner, self).setUp() - self.distro = self._get_distro("mariner", renderers=["networkd"]) +@pytest.fixture +def distro_mariner(mocker): + mocker.patch("cloudinit.net.networkd.util.chownbyname") + return get_distro("mariner", renderers=["networkd"]) + +@pytest.mark.usefixtures("fake_filesystem") +class TestNetCfgDistroMariner: def create_conf_dict(self, contents): content_dict = {} for line in contents: @@ -1231,25 +1325,26 @@ def create_conf_dict(self, contents): def compare_dicts(self, actual, expected): for k, v in actual.items(): - self.assertEqual(sorted(expected[k]), sorted(v)) + assert sorted(expected[k]) == sorted(v) def _apply_and_verify( - self, apply_fn, config, expected_cfgs=None, bringup=False + self, apply_fn, config, tmp_path, expected_cfgs=None, bringup=False ): if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.networkd.available") as m_avail: m_avail.return_value = True - with self.reRooted(tmpd) as tmpd: - apply_fn(config, bringup) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected in expected_cfgs.items(): actual = self.create_conf_dict(results[cfgpath].splitlines()) self.compare_dicts(actual, expected) - self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + assert 0o644 == get_mode(cfgpath, str(tmp_path)) def nwk_file_path(self, ifname): return "/etc/systemd/network/10-cloud-init-%s.network" % ifname @@ -1280,7 +1375,7 @@ def net_cfg_2(self, ifname): ) return ret - def test_mariner_network_config_v1(self): + def test_mariner_network_config_v1(self, distro_mariner, tmp_path): tmp = self.net_cfg_1("eth0").splitlines() expected_eth0 = self.create_conf_dict(tmp) @@ -1293,10 +1388,13 @@ def test_mariner_network_config_v1(self): } self._apply_and_verify( - self.distro.apply_network_config, V1_NET_CFG, expected_cfgs.copy() + distro_mariner.apply_network_config, + V1_NET_CFG, + tmp_path, + expected_cfgs.copy(), ) - def test_mariner_network_config_v2(self): + def test_mariner_network_config_v2(self, distro_mariner, tmp_path): tmp = self.net_cfg_1("eth7").splitlines() expected_eth7 = self.create_conf_dict(tmp) @@ -1309,10 +1407,15 @@ def test_mariner_network_config_v2(self): } self._apply_and_verify( - self.distro.apply_network_config, V2_NET_CFG, expected_cfgs.copy() + distro_mariner.apply_network_config, + V2_NET_CFG, + tmp_path, + expected_cfgs.copy(), ) - def test_mariner_network_config_v1_with_duplicates(self): + def test_mariner_network_config_v1_with_duplicates( + self, distro_mariner, tmp_path + ): expected = """\ [Match] Name=eth0 @@ -1331,15 +1434,21 @@ def test_mariner_network_config_v1_with_duplicates(self): } self._apply_and_verify( - self.distro.apply_network_config, net_cfg, expected_cfgs.copy() + distro_mariner.apply_network_config, + net_cfg, + tmp_path, + expected_cfgs.copy(), ) -class TestNetCfgDistroAzureLinux(TestNetCfgDistroBase): - def setUp(self): - super().setUp() - self.distro = self._get_distro("azurelinux", renderers=["networkd"]) +@pytest.fixture +def distro_azurelinux(mocker): + mocker.patch("cloudinit.net.networkd.util.chownbyname") + return get_distro("azurelinux", renderers=["networkd"]) + +@pytest.mark.usefixtures("fake_filesystem") +class TestNetCfgDistroAzureLinux: def create_conf_dict(self, contents): content_dict = {} for line in contents: @@ -1356,25 +1465,26 @@ def create_conf_dict(self, contents): def compare_dicts(self, actual, expected): for k, v in actual.items(): - self.assertEqual(sorted(expected[k]), sorted(v)) + assert sorted(expected[k]) == sorted(v) def _apply_and_verify( - self, apply_fn, config, expected_cfgs=None, bringup=False + self, apply_fn, config, tmp_path, expected_cfgs=None, bringup=False ): if not expected_cfgs: raise ValueError("expected_cfg must not be None") - tmpd = None with mock.patch("cloudinit.net.networkd.available") as m_avail: m_avail.return_value = True - with self.reRooted(tmpd) as tmpd: - apply_fn(config, bringup) + apply_fn(config, bringup) - results = dir2dict(tmpd) + results = dir2dict( + str(tmp_path), + filter=lambda fn: fn.startswith(str(tmp_path / "etc")), + ) for cfgpath, expected in expected_cfgs.items(): actual = self.create_conf_dict(results[cfgpath].splitlines()) self.compare_dicts(actual, expected) - self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + assert 0o644 == get_mode(cfgpath, str(tmp_path)) def nwk_file_path(self, ifname): return "/etc/systemd/network/10-cloud-init-%s.network" % ifname @@ -1405,7 +1515,7 @@ def net_cfg_2(self, ifname): ) return ret - def test_azurelinux_network_config_v1(self): + def test_azurelinux_network_config_v1(self, distro_azurelinux, tmp_path): tmp = self.net_cfg_1("eth0").splitlines() expected_eth0 = self.create_conf_dict(tmp) @@ -1418,10 +1528,13 @@ def test_azurelinux_network_config_v1(self): } self._apply_and_verify( - self.distro.apply_network_config, V1_NET_CFG, expected_cfgs.copy() + distro_azurelinux.apply_network_config, + V1_NET_CFG, + tmp_path, + expected_cfgs.copy(), ) - def test_azurelinux_network_config_v2(self): + def test_azurelinux_network_config_v2(self, distro_azurelinux, tmp_path): tmp = self.net_cfg_1("eth7").splitlines() expected_eth7 = self.create_conf_dict(tmp) @@ -1434,10 +1547,15 @@ def test_azurelinux_network_config_v2(self): } self._apply_and_verify( - self.distro.apply_network_config, V2_NET_CFG, expected_cfgs.copy() + distro_azurelinux.apply_network_config, + V2_NET_CFG, + tmp_path, + expected_cfgs.copy(), ) - def test_azurelinux_network_config_v1_with_duplicates(self): + def test_azurelinux_network_config_v1_with_duplicates( + self, distro_azurelinux, tmp_path + ): expected = """\ [Match] Name=eth0 @@ -1456,7 +1574,10 @@ def test_azurelinux_network_config_v1_with_duplicates(self): } self._apply_and_verify( - self.distro.apply_network_config, net_cfg, expected_cfgs.copy() + distro_azurelinux.apply_network_config, + net_cfg, + tmp_path, + expected_cfgs.copy(), ) diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 9d16bdd9423..282b59e15e5 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -499,7 +499,7 @@ def populate_dir_with_ts(path, data): os.utime(os.path.sep.join((path, fpath)), (ts, ts)) -def dir2dict(startdir, prefix=None): +def dir2dict(startdir, prefix=None, filter=None): flist = {} if prefix is None: prefix = startdir @@ -507,6 +507,8 @@ def dir2dict(startdir, prefix=None): for fname in files: fpath = os.path.join(root, fname) key = fpath[len(prefix) :] + if filter is not None and not filter(fpath): + continue flist[key] = util.load_text_file(fpath) return flist @@ -652,16 +654,20 @@ def does_not_raise(): yield -def get_distro(dtype, system_info=None): - """Return a Distro class of distro 'dtype'. +def get_distro(dname, system_info=None, /, renderers=None, activators=None): + """Return a Distro class of distro 'dname'. - cfg is format of CFG_BUILTIN['system_info']. + system_info has the format of CFG_BUILTIN['system_info']. - example: get_distro("debian") + Example: get_distro("debian") """ if system_info is None: system_info = copy.deepcopy(settings.CFG_BUILTIN["system_info"]) - system_info["distro"] = dtype + system_info["distro"] = dname + if renderers: + system_info["network"]["renderers"] = renderers + if activators: + system_info["network"]["activators"] = activators paths = helpers.Paths(system_info["paths"]) - distro_cls = distros.fetch(dtype) - return distro_cls(dtype, system_info, paths) + distro_cls = distros.fetch(dname) + return distro_cls(dname, system_info, paths) From aefdbe06b58f4770e2d6d5620a0d0743504ec22a Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 16 Jul 2025 21:57:33 +0200 Subject: [PATCH 025/150] test: drop citestcase from analyze tests (#6312) --- tests/unittests/analyze/test_boot.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/unittests/analyze/test_boot.py b/tests/unittests/analyze/test_boot.py index 1e1ffcf4f92..c25cc8a7b7c 100644 --- a/tests/unittests/analyze/test_boot.py +++ b/tests/unittests/analyze/test_boot.py @@ -9,22 +9,24 @@ SystemctlReader, dist_check_timestamp, ) -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock err_code = (FAIL_CODE, -1, -1, -1) -class TestDistroChecker(CiTestCase): - def test_blank_distro(self): - self.assertEqual(err_code, dist_check_timestamp()) +class TestDistroChecker: + @mock.patch("cloudinit.subp.subp") + def test_blank_distro(self, m_subp): + assert err_code == dist_check_timestamp() + @mock.patch("cloudinit.subp.subp") @mock.patch("cloudinit.util.is_FreeBSD", return_value=True) - def test_freebsd_gentoo_cant_find(self, m_is_FreeBSD): - self.assertEqual(err_code, dist_check_timestamp()) + def test_freebsd_gentoo_cant_find(self, m_is_FreeBSD, m_subp): + err_code == dist_check_timestamp() @mock.patch("cloudinit.subp.subp", return_value=(0, 1)) def test_subp_fails(self, m_subp): - self.assertEqual(err_code, dist_check_timestamp()) + assert err_code == dist_check_timestamp() class TestSystemCtlReader: From 9a480f1ce87a206cb029bbb62c401d28cfc96644 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 28 May 2025 22:11:47 -0500 Subject: [PATCH 026/150] test: simplify passing of lxd_setup callback Also, refactor clouds.py to make pyright typing happier --- tests/integration_tests/clouds.py | 31 ++++++++++++++++------------- tests/integration_tests/conftest.py | 4 +--- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index 1405bae99d0..e31b04a7293 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -7,7 +7,7 @@ import string from abc import ABC, abstractmethod from copy import deepcopy -from typing import Optional, Type +from typing import Callable, Optional from uuid import UUID from pycloudlib import ( @@ -21,10 +21,11 @@ Openstack, Qemu, ) -from pycloudlib.cloud import ImageType +from pycloudlib.cloud import BaseCloud, ImageType from pycloudlib.ec2.instance import EC2Instance -from pycloudlib.lxd.cloud import BaseCloud, _BaseLXD -from pycloudlib.lxd.instance import BaseInstance, LXDInstance +from pycloudlib.instance import BaseInstance +from pycloudlib.lxd.cloud import _BaseLXD +from pycloudlib.lxd.instance import LXDInstance import cloudinit from cloudinit.subp import ProcessExecutionError, subp @@ -140,7 +141,7 @@ def launch( launch_kwargs = {**default_launch_kwargs, **launch_kwargs} display_launch_kwargs = deepcopy(launch_kwargs) if display_launch_kwargs.get("user_data") is not None: - if "token" in display_launch_kwargs.get("user_data"): + if "token" in display_launch_kwargs["user_data"]: display_launch_kwargs["user_data"] = re.sub( r"token: .*", "token: REDACTED", launch_kwargs["user_data"] ) @@ -286,7 +287,6 @@ def _get_cloud_instance(self) -> OCI: class _LxdIntegrationCloud(IntegrationCloud): - pycloudlib_instance_cls: Type[_BaseLXD] instance_tag: str cloud_instance: _BaseLXD @@ -295,7 +295,7 @@ def _get_initial_image(self, **kwargs) -> str: image_type=self._image_type, **kwargs ) - def _get_or_set_profile_list(self, release): + def _get_or_set_profile_list(self, release) -> Optional[list]: return None @staticmethod @@ -332,7 +332,12 @@ def _mount_source(instance: LXDInstance): subp(command.split()) def _perform_launch( - self, *, launch_kwargs, wait=True, **kwargs + self, + *, + launch_kwargs, + wait=True, + lxd_setup: Optional[Callable] = None, + **kwargs, ) -> LXDInstance: instance_kwargs = deepcopy(launch_kwargs) instance_kwargs["inst_type"] = instance_kwargs.pop( @@ -359,9 +364,9 @@ def _perform_launch( ) if self.settings.CLOUD_INIT_SOURCE == "IN_PLACE": self._mount_source(pycloudlib_instance) - if "lxd_setup" in kwargs: + if lxd_setup is not None: log.info("Running callback specified by 'lxd_setup' mark") - kwargs["lxd_setup"](pycloudlib_instance) + lxd_setup(pycloudlib_instance) pycloudlib_instance.start(wait=wait) return pycloudlib_instance @@ -369,22 +374,20 @@ def _perform_launch( class LxdContainerCloud(_LxdIntegrationCloud): datasource = "lxd_container" cloud_instance: LXDContainer - pycloudlib_instance_cls = LXDContainer instance_tag = "lxd-container-integration-test" def _get_cloud_instance(self) -> LXDContainer: - return self.pycloudlib_instance_cls(tag=self.instance_tag) + return LXDContainer(tag=self.instance_tag) class LxdVmCloud(_LxdIntegrationCloud): datasource = "lxd_vm" cloud_instance: LXDVirtualMachine - pycloudlib_instance_cls = LXDVirtualMachine instance_tag = "lxd-vm-integration-test" _profile_list: list = [] def _get_cloud_instance(self) -> LXDVirtualMachine: - return self.pycloudlib_instance_cls(tag=self.instance_tag) + return LXDVirtualMachine(tag=self.instance_tag) def _get_or_set_profile_list(self, release) -> list: if self._profile_list: diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 670817671c9..18dc41d0169 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -315,16 +315,14 @@ def _client( if not isinstance(session_cloud, _LxdIntegrationCloud): pytest.skip("lxd_use_exec requires LXD") launch_kwargs["execute_via_ssh"] = False - local_launch_kwargs = {} if lxd_setup is not None: if not isinstance(session_cloud, _LxdIntegrationCloud): pytest.skip("lxd_setup requires LXD") - local_launch_kwargs["lxd_setup"] = lxd_setup with session_cloud.launch( user_data=user_data, launch_kwargs=launch_kwargs, - **local_launch_kwargs, + lxd_setup=lxd_setup, ) as instance: if lxd_use_exec is not None and isinstance( instance.instance, LXDInstance From 1eb9e5c9186307a50804b3c92ef8e6c960269e7a Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 28 May 2025 23:17:23 -0500 Subject: [PATCH 027/150] test: refactor obtaining session args into new function --- tests/integration_tests/conftest.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 18dc41d0169..d32eb83e96e 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -285,15 +285,7 @@ def _collect_artifacts( _collect_profile(instance, log_dir) -@contextmanager -def _client( - request, fixture_utils, session_cloud: IntegrationCloud -) -> Iterator[IntegrationInstance]: - """Fixture implementation for the client fixtures. - - Launch the dynamic IntegrationClient instance using any provided - userdata, yield to the test, then cleanup - """ +def get_session_args(request, fixture_utils, session_cloud: IntegrationCloud): getter = functools.partial( fixture_utils.closest_marker_first_arg_or, request, default=None ) @@ -318,6 +310,21 @@ def _client( if lxd_setup is not None: if not isinstance(session_cloud, _LxdIntegrationCloud): pytest.skip("lxd_setup requires LXD") + return user_data, launch_kwargs, lxd_setup, lxd_use_exec + + +@contextmanager +def _client( + request, fixture_utils, session_cloud: IntegrationCloud +) -> Iterator[IntegrationInstance]: + """Fixture implementation for the client fixtures. + + Launch the dynamic IntegrationClient instance using any provided + userdata, yield to the test, then cleanup + """ + user_data, launch_kwargs, lxd_setup, lxd_use_exec = get_session_args( + request, fixture_utils, session_cloud + ) with session_cloud.launch( user_data=user_data, From bf233fe7102d3e4fe9da6e453178a52fb77aafcb Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 28 May 2025 23:17:23 -0500 Subject: [PATCH 028/150] test: turn reaper into a fixture The existing reaper implementation was created as a session singleton. This worked but had a few problems: * Requires use of several global variables * Makes use of less documented pytest hooks * Meant that a test session cannot be started without initializing the session_cloud fixture. This can be a fairly long process that shouldn't be run unless necessary * It made it harder to reason about the pytest setup process when both fixtures and custom hooks are involved For these reasons, and to hopefully make an xdist implementation easier, the reaper was converted to be a fixture. --- tests/integration_tests/clouds.py | 3 + tests/integration_tests/conftest.py | 110 ++++++++++--------------- tests/integration_tests/instances.py | 2 +- tests/integration_tests/reaper.py | 2 +- tests/integration_tests/test_reaper.py | 10 +-- 5 files changed, 52 insertions(+), 75 deletions(-) diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index e31b04a7293..a93a2d2da3e 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -31,6 +31,7 @@ from cloudinit.subp import ProcessExecutionError, subp from tests.integration_tests import integration_settings from tests.integration_tests.instances import IntegrationInstance +from tests.integration_tests.reaper import Reaper from tests.integration_tests.releases import CURRENT_RELEASE from tests.integration_tests.util import emit_dots_on_travis @@ -59,9 +60,11 @@ class IntegrationCloud(ABC): def __init__( self, + reaper: Reaper, image_type: ImageType = ImageType.GENERIC, settings=integration_settings, ): + self.reaper = reaper self._image_type = image_type self.settings = settings self.cloud_instance = self._get_cloud_instance() diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index d32eb83e96e..97c19743fb4 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -15,7 +15,6 @@ from pycloudlib.cloud import ImageType from pycloudlib.lxd.instance import LXDInstance -import tests.integration_tests.reaper as reaper from tests.integration_tests import integration_settings from tests.integration_tests.clouds import ( AzureCloud, @@ -34,6 +33,7 @@ CloudInitSource, IntegrationInstance, ) +from tests.integration_tests.reaper import Reaper log = logging.getLogger("integration_testing") log.addHandler(logging.StreamHandler(sys.stdout)) @@ -80,21 +80,8 @@ def disable_subp_usage(request): pass -_SESSION_CLOUD: IntegrationCloud -REAPER: reaper._Reaper - - @pytest.fixture(scope="session") -def session_cloud() -> Generator[IntegrationCloud, None, None]: - """a shared session is created in pytest_sessionstart() - - yield this shared session - """ - global _SESSION_CLOUD - yield _SESSION_CLOUD - - -def get_session_cloud() -> IntegrationCloud: +def session_cloud(reaper: Reaper) -> Generator[IntegrationCloud, None, None]: """get_session_cloud() creates a session from configuration""" if integration_settings.PLATFORM not in platforms.keys(): raise ValueError( @@ -109,9 +96,33 @@ def get_session_cloud() -> IntegrationCloud: f"{integration_settings.OS_IMAGE_TYPE} is an invalid OS_IMAGE_TYPE" f" specified in settings. Must be one of {image_types}" ) - cloud = platforms[integration_settings.PLATFORM](image_type=image_type) + + cloud: IntegrationCloud = platforms[integration_settings.PLATFORM]( + reaper=reaper, image_type=image_type + ) cloud.emit_settings_to_log() - return cloud + + try: + setup_image(cloud) + except Exception as e: + if cloud.snapshot_id: + # if a snapshot id was set, then snapshot succeeded, teardown + cloud.delete_snapshot() + cloud.destroy() + pytest.exit( + f"{type(e).__name__} in session setup: {str(e)}", returncode=2 + ) + yield cloud + log.info("Tearing down session cloud") + try: + cloud.delete_snapshot() + except Exception as e: + log.warning( + "Could not delete snapshot. Leaked snapshot id %s: %s", + cloud.snapshot_id, + e, + ) + cloud.destroy() def get_validated_source( @@ -484,66 +495,29 @@ def _generate_profile_report() -> None: log.info(command, "final.stats") -# https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_sessionstart -def pytest_sessionstart(session) -> None: - """do session setup""" - global _SESSION_CLOUD - global REAPER - log.info("starting session") - try: - _SESSION_CLOUD = get_session_cloud() - except Exception as e: - pytest.exit( - f"{type(e).__name__} in session setup: {str(e)}", returncode=2 - ) +@pytest.fixture(scope="session") +def reaper(): + """Fixture to provide a reaper instance for cleaning up instances.""" + reaper_instance = Reaper() + reaper_instance.start() + + yield reaper_instance + try: - setup_image(_SESSION_CLOUD) - REAPER = reaper._Reaper() - REAPER.start() + reaper_instance.stop() except Exception as e: - if _SESSION_CLOUD: - # if a _SESSION_CLOUD was allocated, clean it up - if _SESSION_CLOUD.snapshot_id: - # if a snapshot id was set, then snapshot succeeded, teardown - _SESSION_CLOUD.delete_snapshot() - _SESSION_CLOUD.destroy() - pytest.exit( - f"{type(e).__name__} in session setup: {str(e)}", returncode=2 + log.warning( + "Could not tear down instance reaper thread: %s(%s)", + type(e).__name__, + e, ) - log.info("started session") def pytest_sessionfinish(session, exitstatus) -> None: - """do session teardown""" - global REAPER - log.info("finishing session") try: if integration_settings.INCLUDE_COVERAGE: _generate_coverage_report() elif integration_settings.INCLUDE_PROFILE: _generate_profile_report() except Exception as e: - log.warning("Could not generate report during teardown: %s", e) - try: - _SESSION_CLOUD.delete_snapshot() - except Exception as e: - log.warning( - "Could not delete snapshot. Leaked snapshot id %s: %s", - _SESSION_CLOUD.snapshot_id, - e, - ) - try: - REAPER.stop() - except Exception as e: - log.warning( - "Could not tear down instance reaper thread: %s(%s)", - type(e).__name__, - e, - ) - try: - _SESSION_CLOUD.destroy() - except Exception as e: - log.warning( - "Could not destroy session cloud: %s(%s)", type(e).__name__, e - ) - log.info("finish session") + log.warning("Could not generate report during session finish: %s", e) diff --git a/tests/integration_tests/instances.py b/tests/integration_tests/instances.py index 70d99a82afb..727323192f2 100644 --- a/tests/integration_tests/instances.py +++ b/tests/integration_tests/instances.py @@ -351,4 +351,4 @@ def __exit__(self, exc_type, exc_val, exc_tb): ): log.info("Keeping Instance, public ip: %s", self.ip()) else: - conftest.REAPER.reap(self) + self.cloud.reaper.reap(self) diff --git a/tests/integration_tests/reaper.py b/tests/integration_tests/reaper.py index 928ed806491..545ceefbca5 100644 --- a/tests/integration_tests/reaper.py +++ b/tests/integration_tests/reaper.py @@ -20,7 +20,7 @@ LOG = logging.getLogger() -class _Reaper: +class Reaper: def __init__(self, timeout: float = 30.0): # self.timeout sets the amount of time to sleep before retrying self.timeout = timeout diff --git a/tests/integration_tests/test_reaper.py b/tests/integration_tests/test_reaper.py index d34b2a191b9..6c463c68f5b 100644 --- a/tests/integration_tests/test_reaper.py +++ b/tests/integration_tests/test_reaper.py @@ -39,7 +39,7 @@ def test_start_stop(self): """basic setup teardown""" instance = MockInstance(0) - r = reaper._Reaper() + r = reaper.Reaper() # start / stop r.start() r.stop() @@ -57,7 +57,7 @@ def test_basic_reap(self): """basic setup teardown""" i_1 = MockInstance(0) - r = reaper._Reaper() + r = reaper.Reaper() r.start() r.reap(i_1) r.stop() @@ -68,7 +68,7 @@ def test_unreaped_instance(self): i_1 = MockInstance(64) i_2 = MockInstance(64) - r = reaper._Reaper() + r = reaper.Reaper() r.start() r.reap(i_1) r.reap(i_2) @@ -94,7 +94,7 @@ def test_stubborn_reap(self): ] # forcibly disallow sleeping, to avoid wasted time during tests - r = reaper._Reaper(timeout=0.0) + r = reaper.Reaper(timeout=0.0) r.start() for i in instances: r.reap(i) @@ -130,7 +130,7 @@ def test_start_stop_multiple(self): """ num = 64 instances = [] - r = reaper._Reaper() + r = reaper.Reaper() r.start() for _ in range(num): i = MockInstance(0) From 753c7a671e801723baddebf045df576bc1bf8512 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Thu, 29 May 2025 21:18:13 -0500 Subject: [PATCH 029/150] test: ensure image setup only runs once, even for xdist --- integration-requirements.txt | 3 ++ tests/integration_tests/conftest.py | 68 +++++++++++++++++++++++----- tests/integration_tests/instances.py | 2 +- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/integration-requirements.txt b/integration-requirements.txt index 01d9f280fa9..0a18907f3a3 100644 --- a/integration-requirements.txt +++ b/integration-requirements.txt @@ -11,6 +11,9 @@ pycloudlib>=1!10.0.2,<1!11 pytest!=7.3.2 pytest-timeout +# Even when xdist is not actively used, we have fixtures that require it +pytest-xdist + packaging passlib coverage==7.2.7 # Last version supported in Python 3.7 diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 97c19743fb4..194cda58d64 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -1,5 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. import datetime +import fcntl import functools import logging import os @@ -80,8 +81,61 @@ def disable_subp_usage(request): pass +def setup_image_or_die(cloud: IntegrationCloud): + try: + setup_image(cloud) + except Exception as e: + if cloud.snapshot_id: + # if a snapshot id was set, then snapshot succeeded, teardown + cloud.delete_snapshot() + cloud.destroy() + pytest.exit( + f"{type(e).__name__} in session setup: {str(e)}", returncode=2 + ) + + +def setup_image_once( + worker_id: str, + cloud: IntegrationCloud, + tmp_path_factory: pytest.TempPathFactory, +) -> None: + """Setup image to be used for (almost) all tests. + + Since pytest-xdist runs tests in parallel, we need to ensure that + the image setup is only done once, and not multiple times in parallel. + """ + if worker_id == "master": + # We're running single-threaded, so no synchronization needed + setup_image_or_die(cloud) + return + + # We're an xdist worker, so we need to synchronize. + # Whoever gets the lock first will do the setup, writing the + # image id into the image path. The other workers will + # wait for file access, read the image id from image path and use it. + image_path = Path( + tmp_path_factory.getbasetemp().parent, + f"session_image_id_{worker_id}", + ) + lock_path = image_path.with_suffix(".lock") + + with open(lock_path, "w") as lock_file: + # Use a lock to ensure only one worker does the setup + fcntl.lockf(lock_file, fcntl.LOCK_EX) + try: + if not image_path.exists(): + setup_image_or_die(cloud) + image_path.write_text(cloud.image_id) + else: + cloud.snapshot_id = image_path.read_text().strip() + finally: + fcntl.lockf(lock_file, fcntl.LOCK_UN) + + @pytest.fixture(scope="session") -def session_cloud(reaper: Reaper) -> Generator[IntegrationCloud, None, None]: +def session_cloud( + worker_id, reaper: Reaper, tmp_path_factory +) -> Generator[IntegrationCloud, None, None]: """get_session_cloud() creates a session from configuration""" if integration_settings.PLATFORM not in platforms.keys(): raise ValueError( @@ -102,16 +156,8 @@ def session_cloud(reaper: Reaper) -> Generator[IntegrationCloud, None, None]: ) cloud.emit_settings_to_log() - try: - setup_image(cloud) - except Exception as e: - if cloud.snapshot_id: - # if a snapshot id was set, then snapshot succeeded, teardown - cloud.delete_snapshot() - cloud.destroy() - pytest.exit( - f"{type(e).__name__} in session setup: {str(e)}", returncode=2 - ) + setup_image_once(worker_id, cloud, tmp_path_factory) + yield cloud log.info("Tearing down session cloud") try: diff --git a/tests/integration_tests/instances.py b/tests/integration_tests/instances.py index 727323192f2..8268a775a2a 100644 --- a/tests/integration_tests/instances.py +++ b/tests/integration_tests/instances.py @@ -15,7 +15,7 @@ from pycloudlib.result import Result from tests.helpers import cloud_init_project_dir -from tests.integration_tests import conftest, integration_settings +from tests.integration_tests import integration_settings from tests.integration_tests.decorators import retry from tests.integration_tests.util import ASSETS_DIR From dfaf5fd69f78e7bbad5e7b2b6646f1fca3dc0e10 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 17 Jul 2025 12:30:24 +0200 Subject: [PATCH 030/150] test: drop FilesystemMockingTestCase (#6294) Now we finally can remove this one fully and forever. Fixes GH-5760 --- tests/unittests/helpers.py | 132 +------------------------------------ 1 file changed, 2 insertions(+), 130 deletions(-) diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 282b59e15e5..75e7c1df34c 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -9,11 +9,10 @@ import random import shutil import string -import sys import tempfile import time import unittest -from contextlib import ExitStack, contextmanager +from contextlib import contextmanager from typing import ClassVar, List, Union from unittest import mock from unittest.util import strclass @@ -21,7 +20,7 @@ import responses -from cloudinit import atomic_helper, distros, helpers, settings, subp, util +from cloudinit import distros, helpers, settings, subp, util from cloudinit.config.schema import ( SchemaValidationError, validate_cloudconfig_schema, @@ -266,133 +265,6 @@ def random_string(cls, length=8): return random_string(length) -class FilesystemMockingTestCase(CiTestCase): - def setUp(self): - super(FilesystemMockingTestCase, self).setUp() - self.patched_funcs = ExitStack() - - def tearDown(self): - self.patched_funcs.close() - CiTestCase.tearDown(self) - - def replicateTestRoot(self, example_root, target_root): - real_root = resourceLocation() - real_root = os.path.join(real_root, "roots", example_root) - for dir_path, _dirnames, filenames in os.walk(real_root): - real_path = dir_path - make_path = rebase_path(real_path[len(real_root) :], target_root) - util.ensure_dir(make_path) - for f in filenames: - real_path = os.path.abspath(os.path.join(real_path, f)) - make_path = os.path.abspath(os.path.join(make_path, f)) - shutil.copy(real_path, make_path) - - def patchUtils(self, new_root): - patch_funcs = { - util: [ - ("write_file", 1), - ("append_file", 1), - ("load_binary_file", 1), - ("load_text_file", 1), - ("ensure_dir", 1), - ("chmod", 1), - ("delete_dir_contents", 1), - ("del_file", 1), - ("sym_link", -1), - ("copy", -1), - ], - atomic_helper: [ - ("write_json", 1), - ], - } - for mod, funcs in patch_funcs.items(): - for f, am in funcs: - func = getattr(mod, f) - trap_func = retarget_many_wrapper(new_root, am, func) - self.patched_funcs.enter_context( - mock.patch.object(mod, f, trap_func) - ) - - # Handle subprocess calls - func = getattr(subp, "subp") - - def nsubp(*_args, **_kwargs): - return ("", "") - - self.patched_funcs.enter_context( - mock.patch.object(subp, "subp", nsubp) - ) - - def null_func(*_args, **_kwargs): - return None - - for f in ["chownbyid", "chownbyname"]: - self.patched_funcs.enter_context( - mock.patch.object(util, f, null_func) - ) - - def patchOS(self, new_root): - patch_funcs = { - os.path: [ - ("isfile", 1), - ("exists", 1), - ("islink", 1), - ("isdir", 1), - ("lexists", 1), - ], - os: [ - ("listdir", 1), - ("mkdir", 1), - ("lstat", 1), - ("symlink", 2), - ("stat", 1), - ], - } - - if hasattr(os, "scandir"): - # py27 does not have scandir - patch_funcs[os].append(("scandir", 1)) - - for mod, funcs in patch_funcs.items(): - for f, nargs in funcs: - func = getattr(mod, f) - trap_func = retarget_many_wrapper(new_root, nargs, func) - self.patched_funcs.enter_context( - mock.patch.object(mod, f, trap_func) - ) - - def patchOpen(self, new_root): - trap_func = retarget_many_wrapper(new_root, 1, open) - self.patched_funcs.enter_context( - mock.patch("builtins.open", trap_func) - ) - - def patchStdoutAndStderr(self, stdout=None, stderr=None): - if stdout is not None: - self.patched_funcs.enter_context( - mock.patch.object(sys, "stdout", stdout) - ) - if stderr is not None: - self.patched_funcs.enter_context( - mock.patch.object(sys, "stderr", stderr) - ) - - def reRoot(self, root=None): - if root is None: - root = self.tmp_dir() - self.patchUtils(root) - self.patchOS(root) - self.patchOpen(root) - return root - - @contextmanager - def reRooted(self, root=None): - try: - yield self.reRoot(root) - finally: - self.patched_funcs.close() - - def replicate_test_root(example_root, target_root): real_root = resourceLocation() real_root = os.path.join(real_root, "roots", example_root) From 7d602e0a8f16b5dd7a1468e253dfd15a1aa24481 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Thu, 17 Jul 2025 08:54:45 -0500 Subject: [PATCH 031/150] ci: update patch checking to use current supported releases (#6315) --- .github/workflows/packaging-upstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packaging-upstream.yml b/.github/workflows/packaging-upstream.yml index 3a7429d19d8..2caa12cf38b 100644 --- a/.github/workflows/packaging-upstream.yml +++ b/.github/workflows/packaging-upstream.yml @@ -49,7 +49,7 @@ jobs: - name: Run test - apply patches and run unit tests for each series run: | # Modify the following line to add / remove ubuntu series - for BRANCH in ubuntu/devel ubuntu/oracular ubuntu/noble ubuntu/jammy ubuntu/focal; do + for BRANCH in ubuntu/devel ubuntu/plucky ubuntu/noble ubuntu/jammy; do # merge - this step is not expected to fail git merge "origin/$BRANCH" if [ ! -f debian/patches/series ]; then From 2dab1fdcb1ae9a2ca2b6e719826bb3bd03df652c Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 17 Jul 2025 15:59:06 +0200 Subject: [PATCH 032/150] test: drop citestcase from rest of config tests (#6314) --- .../config/test_cc_rh_subscription.py | 217 ++++++++---------- tests/unittests/config/test_cc_snap.py | 24 +- .../unittests/config/test_cc_users_groups.py | 58 ++--- tests/unittests/config/test_cc_wireguard.py | 140 +++++------ tests/unittests/config/test_cc_write_files.py | 27 +-- tests/unittests/config/test_schema.py | 21 +- tests/unittests/helpers.py | 10 + 7 files changed, 225 insertions(+), 272 deletions(-) diff --git a/tests/unittests/config/test_cc_rh_subscription.py b/tests/unittests/config/test_cc_rh_subscription.py index d811d16a5b6..0f4676170d6 100644 --- a/tests/unittests/config/test_cc_rh_subscription.py +++ b/tests/unittests/config/test_cc_rh_subscription.py @@ -14,52 +14,45 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema +from tests.unittests.helpers import mock, skipUnlessJsonSchema SUBMGR = cc_rh_subscription.SubscriptionManager SUB_MAN_CLI = "cloudinit.config.cc_rh_subscription._sub_man_cli" +NAME = "cc_rh_subscription" @mock.patch(SUB_MAN_CLI) -class GoodTests(CiTestCase): - with_logs = True +class TestHappyPath: - def setUp(self): - super(GoodTests, self).setUp() - self.name = "cc_rh_subscription" - self.cloud_init = None - self.log = logging.getLogger("good_tests") - self.args = [] - self.handle = cc_rh_subscription.handle - - self.config = { - "rh_subscription": { - "username": "scooby@do.com", - "password": "scooby-snacks", - } + CONFIG = { + "rh_subscription": { + "username": "scooby@do.com", + "password": "scooby-snacks", } - self.config_full = { - "rh_subscription": { - "username": "scooby@do.com", - "password": "scooby-snacks", - "auto-attach": True, - "service-level": "self-support", - "add-pool": ["pool1", "pool2", "pool3"], - "enable-repo": ["repo1", "repo2", "repo3"], - "disable-repo": ["repo4", "repo5"], - } + } + + CONFIG_FULL = { + "rh_subscription": { + "username": "scooby@do.com", + "password": "scooby-snacks", + "auto-attach": True, + "service-level": "self-support", + "add-pool": ["pool1", "pool2", "pool3"], + "enable-repo": ["repo1", "repo2", "repo3"], + "disable-repo": ["repo4", "repo5"], } + } - def test_already_registered(self, m_sman_cli): + def test_already_registered(self, m_sman_cli, caplog): """ Emulates a system that is already registered. Ensure it gets a non-ProcessExecution error from is_registered() """ - self.handle(self.name, self.config, self.cloud_init, self.args) - self.assertEqual(m_sman_cli.call_count, 1) - self.assertIn("System is already registered", self.logs.getvalue()) + cc_rh_subscription.handle(NAME, self.CONFIG, None, []) + assert m_sman_cli.call_count == 1 + assert "System is already registered" in caplog.text - def test_simple_registration(self, m_sman_cli): + def test_simple_registration(self, m_sman_cli, caplog): """ Simple registration with username and password """ @@ -68,9 +61,9 @@ def test_simple_registration(self, m_sman_cli): " 12345678-abde-abcde-1234-1234567890abc" ) m_sman_cli.side_effect = [subp.ProcessExecutionError, (reg, "bar")] - self.handle(self.name, self.config, self.cloud_init, self.args) - self.assertIn(mock.call(["identity"]), m_sman_cli.call_args_list) - self.assertIn( + cc_rh_subscription.handle(NAME, self.CONFIG, None, []) + assert mock.call(["identity"]) in m_sman_cli.call_args_list + assert ( mock.call( [ "register", @@ -78,30 +71,27 @@ def test_simple_registration(self, m_sman_cli): "--password=scooby-snacks", ], logstring_val=True, - ), - m_sman_cli.call_args_list, - ) - self.assertIn( - "rh_subscription plugin completed successfully", - self.logs.getvalue(), + ) + in m_sman_cli.call_args_list ) - self.assertEqual(m_sman_cli.call_count, 2) + assert "rh_subscription plugin completed successfully" in caplog.text + assert m_sman_cli.call_count == 2 @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_getRepos") def test_update_repos_disable_with_none(self, m_get_repos, m_sman_cli): - cfg = copy.deepcopy(self.config) + cfg = copy.deepcopy(self.CONFIG) m_get_repos.return_value = ([], ["repo1"]) cfg["rh_subscription"].update( {"enable-repo": ["repo1"], "disable-repo": None} ) mysm = cc_rh_subscription.SubscriptionManager(cfg) - self.assertEqual(True, mysm.update_repos()) + assert True is mysm.update_repos() m_get_repos.assert_called_with() - self.assertEqual( - m_sman_cli.call_args_list, [mock.call(["repos", "--enable=repo1"])] - ) + assert m_sman_cli.call_args_list == [ + mock.call(["repos", "--enable=repo1"]) + ] - def test_full_registration(self, m_sman_cli): + def test_full_registration(self, m_sman_cli, caplog): """ Registration with auto-attach, service-level, adding pools, and enabling and disabling yum repos @@ -127,38 +117,30 @@ def test_full_registration(self, m_sman_cli): ("Repo ID: repo2\nRepo ID: repo3\nRepo ID: repo4", ""), ("", ""), ] - self.handle(self.name, self.config_full, self.cloud_init, self.args) - self.assertEqual(m_sman_cli.call_count, 9) + cc_rh_subscription.handle(NAME, self.CONFIG_FULL, None, []) + assert m_sman_cli.call_count == 9 for call in call_lists: - self.assertIn(mock.call(call), m_sman_cli.call_args_list) - self.assertIn( - "rh_subscription plugin completed successfully", - self.logs.getvalue(), - ) + assert mock.call(call) in m_sman_cli.call_args_list + assert "rh_subscription plugin completed successfully" in caplog.text @mock.patch(SUB_MAN_CLI) -class TestBadInput(CiTestCase): - with_logs = True - name = "cc_rh_subscription" - cloud_init = None - log = logging.getLogger("bad_tests") - args: list = [] +class TestBadInput: SM = cc_rh_subscription.SubscriptionManager - reg = ( + REG = ( "The system has been registered with ID:" " 12345678-abde-abcde-1234-1234567890abc" ) - config_no_password = {"rh_subscription": {"username": "scooby@do.com"}} + CONFIG_NO_PASSWORD = {"rh_subscription": {"username": "scooby@do.com"}} - config_no_key = { + CONFIG_NO_KEY = { "rh_subscription": { "activation-key": "1234abcde", } } - config_service = { + CONFIG_SERVICE = { "rh_subscription": { "username": "scooby@do.com", "password": "scooby-snacks", @@ -166,21 +148,21 @@ class TestBadInput(CiTestCase): } } - config_badpool = { + CONFIG_BADPOOL = { "rh_subscription": { "username": "scooby@do.com", "password": "scooby-snacks", "add-pool": "not_a_list", } } - config_badrepo = { + CONFIG_BADREPO = { "rh_subscription": { "username": "scooby@do.com", "password": "scooby-snacks", "enable-repo": "not_a_list", } } - config_badkey = { + CONFIG_BADKEY = { "rh_subscription": { "activation-key": "abcdef1234", "fookey": "bar", @@ -188,125 +170,112 @@ class TestBadInput(CiTestCase): } } - def setUp(self): - super(TestBadInput, self).setUp() - self.handle = cc_rh_subscription.handle - - def assert_logged_warnings(self, warnings): - logs = self.logs.getvalue() - missing = [w for w in warnings if "WARNING: " + w not in logs] - self.assertEqual([], missing, "Missing expected warnings.") + def assert_logged_warnings(self, warnings, caplog): + missing = [ + w + for w in warnings + if (mock.ANY, logging.WARNING, w) not in caplog.record_tuples + ] + assert [] == missing, "Missing expected warnings." def test_no_password(self, m_sman_cli): """Attempt to register without the password key/value.""" m_sman_cli.side_effect = [ subp.ProcessExecutionError, - (self.reg, "bar"), + (self.REG, "bar"), ] - self.handle( - self.name, - self.config_no_password, - self.cloud_init, - self.args, - ) - self.assertEqual(m_sman_cli.call_count, 0) + cc_rh_subscription.handle(NAME, self.CONFIG_NO_PASSWORD, None, []) + assert m_sman_cli.call_count == 0 - def test_no_org(self, m_sman_cli): + def test_no_org(self, m_sman_cli, caplog): """Attempt to register without the org key/value.""" m_sman_cli.side_effect = [subp.ProcessExecutionError] - self.handle(self.name, self.config_no_key, self.cloud_init, self.args) + cc_rh_subscription.handle(NAME, self.CONFIG_NO_KEY, None, []) m_sman_cli.assert_called_with(["identity"]) - self.assertEqual(m_sman_cli.call_count, 1) + assert m_sman_cli.call_count == 1 self.assert_logged_warnings( ( "Unable to register system due to incomplete information.", "Use either activationkey and org *or* userid and password", "Registration failed or did not run completely", "rh_subscription plugin did not complete successfully", - ) + ), + caplog, ) - def test_service_level_without_auto(self, m_sman_cli): + def test_service_level_without_auto(self, m_sman_cli, caplog): """Attempt to register using service-level without auto-attach key.""" m_sman_cli.side_effect = [ subp.ProcessExecutionError, - (self.reg, "bar"), + (self.REG, "bar"), ] - self.handle( - self.name, - self.config_service, - self.cloud_init, - self.args, - ) - self.assertEqual(m_sman_cli.call_count, 1) + cc_rh_subscription.handle(NAME, self.CONFIG_SERVICE, None, []) + assert m_sman_cli.call_count == 1 self.assert_logged_warnings( ( - "The service-level key must be used in conjunction with ", + "The service-level key must be used in conjunction with the" + " auto-attach key. Please re-run with auto-attach: True", "rh_subscription plugin did not complete successfully", - ) + ), + caplog, ) - def test_pool_not_a_list(self, m_sman_cli): + def test_pool_not_a_list(self, m_sman_cli, caplog): """ Register with pools that are not in the format of a list """ m_sman_cli.side_effect = [ subp.ProcessExecutionError, - (self.reg, "bar"), + (self.REG, "bar"), ] - self.handle( - self.name, - self.config_badpool, - self.cloud_init, - self.args, - ) - self.assertEqual(m_sman_cli.call_count, 2) + cc_rh_subscription.handle(NAME, self.CONFIG_BADPOOL, None, []) + assert m_sman_cli.call_count == 2 self.assert_logged_warnings( ( "Pools must in the format of a list", "rh_subscription plugin did not complete successfully", - ) + ), + caplog, ) - def test_repo_not_a_list(self, m_sman_cli): + def test_repo_not_a_list(self, m_sman_cli, caplog): """ Register with repos that are not in the format of a list """ m_sman_cli.side_effect = [ subp.ProcessExecutionError, - (self.reg, "bar"), + (self.REG, "bar"), ] - self.handle( - self.name, - self.config_badrepo, - self.cloud_init, - self.args, - ) - self.assertEqual(m_sman_cli.call_count, 2) + cc_rh_subscription.handle(NAME, self.CONFIG_BADREPO, None, []) + assert m_sman_cli.call_count == 2 self.assert_logged_warnings( ( "Repo IDs must in the format of a list.", "Unable to add or remove repos", "rh_subscription plugin did not complete successfully", - ) + ), + caplog, ) - def test_bad_key_value(self, m_sman_cli): + def test_bad_key_value(self, m_sman_cli, caplog): """ Attempt to register with a key that we don't know """ m_sman_cli.side_effect = [ subp.ProcessExecutionError, - (self.reg, "bar"), + (self.REG, "bar"), ] - self.handle(self.name, self.config_badkey, self.cloud_init, self.args) - self.assertEqual(m_sman_cli.call_count, 1) + cc_rh_subscription.handle(NAME, self.CONFIG_BADKEY, None, []) + assert m_sman_cli.call_count == 1 self.assert_logged_warnings( ( "fookey is not a valid key for rh_subscription. Valid keys" - " are:", + " are: org, activation-key, username, password, disable-repo," + " enable-repo, add-pool, rhsm-baseurl, server-hostname," + " auto-attach, service-level", "rh_subscription plugin did not complete successfully", - ) + ), + caplog, ) diff --git a/tests/unittests/config/test_cc_snap.py b/tests/unittests/config/test_cc_snap.py index c5026783b22..e170541de4c 100644 --- a/tests/unittests/config/test_cc_snap.py +++ b/tests/unittests/config/test_cc_snap.py @@ -14,7 +14,6 @@ ) from tests.unittests.helpers import ( SCHEMA_EMPTY_ERROR, - CiTestCase, mock, skipUnlessJsonSchema, ) @@ -171,29 +170,22 @@ def test_add_assertions_adds_assertions_as_dict( ) -class TestRunCommands(CiTestCase): - with_logs = True - allowed_subp = [CiTestCase.SUBP_SHELL_TRUE] - - def setUp(self): - super(TestRunCommands, self).setUp() - self.tmp = self.tmp_dir() - +@pytest.mark.usefixtures("fake_filesystem") +class TestRunCommands: @mock.patch("cloudinit.config.cc_snap.subp.subp") - def test_run_commands_on_empty_list(self, m_subp): + def test_run_commands_on_empty_list(self, m_subp, caplog): """When provided with an empty list, run_commands does nothing.""" run_commands([]) - self.assertEqual("", self.logs.getvalue()) + assert "" == caplog.text m_subp.assert_not_called() def test_run_commands_on_non_list_or_dict(self): """When provided an invalid type, run_commands raises an error.""" - with self.assertRaises(TypeError) as context_manager: + with pytest.raises( + TypeError, + match="commands parameter was not a list or dict: I'm Not Valid", + ): run_commands(commands="I'm Not Valid") - self.assertEqual( - "commands parameter was not a list or dict: I'm Not Valid", - str(context_manager.exception), - ) @pytest.mark.allow_all_subp diff --git a/tests/unittests/config/test_cc_users_groups.py b/tests/unittests/config/test_cc_users_groups.py index 7327d5b4f0b..3f8f799e6e4 100644 --- a/tests/unittests/config/test_cc_users_groups.py +++ b/tests/unittests/config/test_cc_users_groups.py @@ -1,4 +1,5 @@ # This file is part of cloud-init. See LICENSE file for license information. +import logging import re import pytest @@ -10,7 +11,7 @@ validate_cloudconfig_schema, ) from tests.unittests.helpers import ( - CiTestCase, + assert_count_equal, does_not_raise, mock, skipUnlessJsonSchema, @@ -22,11 +23,9 @@ @mock.patch("cloudinit.distros.ubuntu.Distro.create_group") @mock.patch("cloudinit.distros.ubuntu.Distro.create_user") -class TestHandleUsersGroups(CiTestCase): +class TestHandleUsersGroups: """Test cc_users_groups handling of config.""" - with_logs = True - def test_handle_no_cfg_creates_no_users_or_groups(self, m_user, m_group): """Test handle with no config will not create users or groups.""" cfg = {} # merged cloud-config @@ -60,7 +59,7 @@ def test_handle_users_in_cfg_calls_create_users(self, m_user, m_group): metadata = {} cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) - self.assertCountEqual( + assert_count_equal( m_user.call_args_list, [ mock.call( @@ -106,7 +105,7 @@ def test_handle_users_in_cfg_calls_create_users_on_bsd( distro="freebsd", sys_cfg=sys_cfg, metadata=metadata ) cc_users_groups.handle("modulename", cfg, cloud, None) - self.assertCountEqual( + assert_count_equal( m_fbsd_user.call_args_list, [ mock.call( @@ -140,7 +139,7 @@ def test_users_with_ssh_redirect_user_passes_keys(self, m_user, m_group): metadata = {"public-keys": ["key1"]} cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) - self.assertCountEqual( + assert_count_equal( m_user.call_args_list, [ mock.call( @@ -179,7 +178,7 @@ def test_users_with_ssh_redirect_user_default_str(self, m_user, m_group): metadata = {"public-keys": ["key1"]} cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) - self.assertCountEqual( + assert_count_equal( m_user.call_args_list, [ mock.call( @@ -210,14 +209,13 @@ def test_users_without_home_cannot_import_ssh_keys(self, m_user, m_group): ] } cloud = get_cloud(distro="ubuntu", sys_cfg={}, metadata={}) - with self.assertRaises(ValueError) as context_manager: + with pytest.raises( + ValueError, + match=r"Not creating user me2. Key\(s\) ssh_import_id cannot be" + " provided with no_create_home", + ): cc_users_groups.handle("modulename", cfg, cloud, None) m_group.assert_not_called() - self.assertEqual( - "Not creating user me2. Key(s) ssh_import_id cannot be provided" - " with no_create_home", - str(context_manager.exception), - ) def test_users_with_ssh_redirect_user_non_default(self, m_user, m_group): """Warn when ssh_redirect_user is not 'default'.""" @@ -238,14 +236,13 @@ def test_users_with_ssh_redirect_user_non_default(self, m_user, m_group): } metadata = {"public-keys": ["key1"]} cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) - with self.assertRaises(ValueError) as context_manager: + with pytest.raises( + ValueError, + match="Not creating user me2. Invalid value of ssh_redirect_user:" + " snowflake. Expected values: true, default or false.", + ): cc_users_groups.handle("modulename", cfg, cloud, None) m_group.assert_not_called() - self.assertEqual( - "Not creating user me2. Invalid value of ssh_redirect_user:" - " snowflake. Expected values: true, default or false.", - str(context_manager.exception), - ) def test_users_with_ssh_redirect_user_default_false(self, m_user, m_group): """When unspecified ssh_redirect_user is false and not set up.""" @@ -262,7 +259,7 @@ def test_users_with_ssh_redirect_user_default_false(self, m_user, m_group): metadata = {"public-keys": ["key1"]} cloud = get_cloud(distro="ubuntu", sys_cfg=sys_cfg, metadata=metadata) cc_users_groups.handle("modulename", cfg, cloud, None) - self.assertCountEqual( + assert_count_equal( m_user.call_args_list, [ mock.call( @@ -276,7 +273,9 @@ def test_users_with_ssh_redirect_user_default_false(self, m_user, m_group): ) m_group.assert_not_called() - def test_users_ssh_redirect_user_and_no_default(self, m_user, m_group): + def test_users_ssh_redirect_user_and_no_default( + self, m_user, m_group, caplog + ): """Warn when ssh_redirect_user is True and no default user present.""" cfg = { "users": ["default", {"name": "me2", "ssh_redirect_user": True}] @@ -288,12 +287,15 @@ def test_users_ssh_redirect_user_and_no_default(self, m_user, m_group): cc_users_groups.handle("modulename", cfg, cloud, None) m_user.assert_called_once_with("me2", default=False) m_group.assert_not_called() - self.assertEqual( - "WARNING: Ignoring ssh_redirect_user: True for me2. No" - " default_user defined. Perhaps missing" - " cloud configuration users: [default, ..].\n", - self.logs.getvalue(), - ) + assert [ + ( + mock.ANY, + logging.WARNING, + "Ignoring ssh_redirect_user: True for me2. No" + " default_user defined. Perhaps missing" + " cloud configuration users: [default, ..].", + ) + ] == caplog.record_tuples class TestUsersGroupsSchema: diff --git a/tests/unittests/config/test_cc_wireguard.py b/tests/unittests/config/test_cc_wireguard.py index 6f46653aa47..6702eb9385a 100644 --- a/tests/unittests/config/test_cc_wireguard.py +++ b/tests/unittests/config/test_cc_wireguard.py @@ -1,4 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. +import logging + import pytest from cloudinit import subp, util @@ -8,7 +10,7 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema +from tests.unittests.helpers import mock, skipUnlessJsonSchema NL = "\n" # Module path used in mocks @@ -21,27 +23,18 @@ def __init__(self, distro): self.distro = distro -class TestWireGuard(CiTestCase): - with_logs = True - allowed_subp = [CiTestCase.SUBP_SHELL_TRUE] - - def setUp(self): - super(TestWireGuard, self).setUp() - self.tmp = self.tmp_dir() - +@pytest.mark.usefixtures("fake_filesystem") +class TestWireGuard: def test_readiness_probe_schema_non_string_values(self): """ValueError raised for any values expected as string type.""" wg_readinessprobes = [1, ["not-a-valid-command"]] - errors = [ - "Expected a string for readinessprobe at 0. Found 1", - "Expected a string for readinessprobe at 1." - " Found ['not-a-valid-command']", - ] - with self.assertRaises(ValueError) as context_mgr: + error = ( + r"Expected a string for readinessprobe at 0. Found 1" + r"\nExpected a string for readinessprobe at 1." + r" Found \['not-a-valid-command'\]" + ) + with pytest.raises(ValueError, match=error): cc_wireguard.readinessprobe_command_validation(wg_readinessprobes) - error_msg = str(context_mgr.exception) - for error in errors: - self.assertIn(error, error_msg) def test_suppl_schema_error_on_missing_keys(self): """ValueError raised reporting any missing required keys""" @@ -50,33 +43,30 @@ def test_suppl_schema_error_on_missing_keys(self): f"Invalid wireguard interface configuration:{NL}" "Missing required wg:interfaces keys: config_path, content, name" ) - with self.assertRaisesRegex(ValueError, match): + with pytest.raises(ValueError, match=match): cc_wireguard.supplemental_schema_validation(cfg) def test_suppl_schema_error_on_non_string_values(self): """ValueError raised for any values expected as string type.""" cfg = {"name": 1, "config_path": 2, "content": 3} - errors = [ - "Expected a string for wg:interfaces:config_path. Found 2", - "Expected a string for wg:interfaces:content. Found 3", - "Expected a string for wg:interfaces:name. Found 1", - ] - with self.assertRaises(ValueError) as context_mgr: + error = ( + "Expected a string for wg:interfaces:config_path. Found 2\n" + "Expected a string for wg:interfaces:content. Found 3\n" + "Expected a string for wg:interfaces:name. Found 1" + ) + with pytest.raises(ValueError, match=error): cc_wireguard.supplemental_schema_validation(cfg) - error_msg = str(context_mgr.exception) - for error in errors: - self.assertIn(error, error_msg) def test_write_config_failed(self): """Errors when writing config are raised.""" wg_int = {"name": "wg0", "config_path": "/no/valid/path"} - with self.assertRaises(RuntimeError) as context_mgr: + with pytest.raises( + RuntimeError, + match="Failure writing Wireguard configuration file" + " /no/valid/path:\n", + ): cc_wireguard.write_config(wg_int) - self.assertIn( - "Failure writing Wireguard configuration file /no/valid/path:\n", - str(context_mgr.exception), - ) @mock.patch("%s.subp.subp" % MPATH) def test_readiness_probe_invalid_command(self, m_subp): @@ -92,16 +82,15 @@ def fake_subp(cmd, capture=None, shell=None): m_subp.side_effect = fake_subp - with self.assertRaises(RuntimeError) as context_mgr: - cc_wireguard.readinessprobe(wg_readinessprobes) - self.assertIn( + error = ( "Failed running readinessprobe command:\n" "not-a-valid-command: Unexpected error while" " running command.\n" "Command: -\nExit code: -\nReason: -\n" - "Stdout: not-a-valid-command: command not found\nStderr: -", - str(context_mgr.exception), + "Stdout: not-a-valid-command: command not found\nStderr: -" ) + with pytest.raises(RuntimeError, match=error): + cc_wireguard.readinessprobe(wg_readinessprobes) @mock.patch("%s.subp.subp" % MPATH) def test_enable_wg_on_error(self, m_subp): @@ -112,16 +101,15 @@ def test_enable_wg_on_error(self, m_subp): "systemctl start wg-quik@wg0 failed: exit code 1" ) mycloud = FakeCloud(distro) - with self.assertRaises(RuntimeError) as context_mgr: - cc_wireguard.enable_wg(wg_int, mycloud) - self.assertEqual( - "Failed enabling/starting Wireguard interface(s):\n" + error = ( + r"Failed enabling/starting Wireguard interface\(s\):\n" "Unexpected error while running command.\n" "Command: -\nExit code: -\nReason: -\n" "Stdout: systemctl start wg-quik@wg0 failed: exit code 1\n" - "Stderr: -", - str(context_mgr.exception), + "Stderr: -" ) + with pytest.raises(RuntimeError, match=error): + cc_wireguard.enable_wg(wg_int, mycloud) @mock.patch("%s.subp.which" % MPATH) def test_maybe_install_wg_packages_noop_when_wg_tools_present( @@ -138,7 +126,7 @@ def test_maybe_install_wg_packages_noop_when_wg_tools_present( @mock.patch("%s.subp.which" % MPATH) @mock.patch("%s.util.kernel_version" % MPATH) def test_maybe_install_wf_tools_raises_update_errors( - self, m_kernel_version, m_which + self, m_kernel_version, m_which, caplog ): """maybe_install_wireguard_packages logs and raises apt update errors.""" @@ -148,17 +136,16 @@ def test_maybe_install_wf_tools_raises_update_errors( distro.update_package_sources.side_effect = RuntimeError( "Some apt error" ) - with self.assertRaises(RuntimeError) as context_manager: + with pytest.raises(RuntimeError, match="Some apt error"): cc_wireguard.maybe_install_wireguard_packages( cloud=FakeCloud(distro) ) - self.assertEqual("Some apt error", str(context_manager.exception)) - self.assertIn("Package update failed\nTraceback", self.logs.getvalue()) + assert "Package update failed\nTraceback" in caplog.text @mock.patch("%s.subp.which" % MPATH) @mock.patch("%s.util.kernel_version" % MPATH) def test_maybe_install_wg_raises_install_errors( - self, m_kernel_version, m_which + self, m_kernel_version, m_which, caplog ): """maybe_install_wireguard_packages logs and raises package install errors.""" @@ -169,34 +156,32 @@ def test_maybe_install_wg_raises_install_errors( distro.install_packages.side_effect = RuntimeError( "Some install error" ) - with self.assertRaises(RuntimeError) as context_manager: + with pytest.raises(RuntimeError, match="Some install error"): cc_wireguard.maybe_install_wireguard_packages( cloud=FakeCloud(distro) ) - self.assertEqual("Some install error", str(context_manager.exception)) - self.assertIn( - "Failed to install wireguard-tools\n", self.logs.getvalue() - ) + assert "Failed to install wireguard-tools\n" in caplog.text @mock.patch("%s.subp.subp" % MPATH) - def test_load_wg_module_failed(self, m_subp): + def test_load_wg_module_failed(self, m_subp, caplog): """load_wireguard_kernel_module logs and raises kernel modules loading error.""" m_subp.side_effect = subp.ProcessExecutionError( "Some kernel module load error" ) - with self.assertRaises(subp.ProcessExecutionError) as context_manager: - cc_wireguard.load_wireguard_kernel_module() - self.assertEqual( + error = ( "Unexpected error while running command.\n" "Command: -\nExit code: -\nReason: -\n" "Stdout: Some kernel module load error\n" - "Stderr: -", - str(context_manager.exception), - ) - self.assertIn( - "WARNING: Could not load wireguard module:\n", self.logs.getvalue() + "Stderr: -" ) + with pytest.raises(subp.ProcessExecutionError, match=error): + cc_wireguard.load_wireguard_kernel_module() + assert ( + mock.ANY, + logging.WARNING, + "Could not load wireguard module:\n" + error, + ) in caplog.record_tuples @mock.patch("%s.subp.which" % MPATH) @mock.patch("%s.util.kernel_version" % MPATH) @@ -217,29 +202,28 @@ def test_maybe_install_wg_packages_happy_path( distro.install_packages.assert_called_once_with(packages) @mock.patch("%s.maybe_install_wireguard_packages" % MPATH) - def test_handle_no_config(self, m_maybe_install_wireguard_packages): + def test_handle_no_config( + self, m_maybe_install_wireguard_packages, caplog + ): """When no wireguard configuration is provided, nothing happens.""" cfg = {} cc_wireguard.handle("wg", cfg=cfg, cloud=None, args=None) - self.assertIn( - "DEBUG: Skipping module named wg, no 'wireguard'" - " configuration found", - self.logs.getvalue(), - ) - self.assertEqual(m_maybe_install_wireguard_packages.call_count, 0) + assert ( + mock.ANY, + logging.DEBUG, + "Skipping module named wg, no 'wireguard' configuration found", + ) in caplog.record_tuples + assert m_maybe_install_wireguard_packages.call_count == 0 def test_readiness_probe_with_non_string_values(self): """ValueError raised for any values expected as string type.""" cfg = [1, 2] - errors = [ - "Expected a string for readinessprobe at 0. Found 1", - "Expected a string for readinessprobe at 1. Found 2", - ] - with self.assertRaises(ValueError) as context_manager: + error = ( + "Expected a string for readinessprobe at 0. Found 1\n" + "Expected a string for readinessprobe at 1. Found 2" + ) + with pytest.raises(ValueError, match=error): cc_wireguard.readinessprobe_command_validation(cfg) - error_msg = str(context_manager.exception) - for error in errors: - self.assertIn(error, error_msg) class TestWireguardSchema: diff --git a/tests/unittests/config/test_cc_write_files.py b/tests/unittests/config/test_cc_write_files.py index 41844f926ef..398fea66d8f 100644 --- a/tests/unittests/config/test_cc_write_files.py +++ b/tests/unittests/config/test_cc_write_files.py @@ -17,11 +17,7 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import ( - SCHEMA_EMPTY_ERROR, - CiTestCase, - skipUnlessJsonSchema, -) +from tests.unittests.helpers import SCHEMA_EMPTY_ERROR, skipUnlessJsonSchema from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) @@ -265,31 +261,32 @@ def test_deferred(self, m_chownbyname, cloud): assert [] == m_chownbyname.call_args_list -class TestDecodePerms(CiTestCase): - - with_logs = True - +class TestDecodePerms: def test_none_returns_default(self): """If None is passed as perms, then default should be returned.""" default = object() found = decode_perms(None, default) - self.assertEqual(default, found) + assert default == found def test_integer(self): """A valid integer should return itself.""" found = decode_perms(0o755, None) - self.assertEqual(0o755, found) + assert 0o755 == found def test_valid_octal_string(self): """A string should be read as octal.""" found = decode_perms("644", None) - self.assertEqual(0o644, found) + assert 0o644 == found - def test_invalid_octal_string_returns_default_and_warns(self): + def test_invalid_octal_string_returns_default_and_warns(self, caplog): """A string with invalid octal should warn and return default.""" found = decode_perms("999", None) - self.assertIsNone(found) - self.assertIn("WARNING: Undecodable", self.logs.getvalue()) + assert found is None + assert ( + mock.ANY, + logging.WARNING, + "Undecodable permissions '999', returning default None", + ) in caplog.record_tuples def _gzip_bytes(data): diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py index c46802b2adb..d158761e5e3 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -47,7 +47,6 @@ from tests.hypothesis import given from tests.hypothesis_jsonschema import from_schema from tests.unittests.helpers import ( - CiTestCase, does_not_raise, mock, skipUnlessHypothesisJsonSchema, @@ -183,13 +182,13 @@ def check_deprecation_keys(schema, search_key): }, "new", ) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): check_deprecation_keys({"changed": True}, "changed") - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): check_deprecation_keys( {"properties": {"deprecated": True}}, "deprecated" ) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): check_deprecation_keys( {"properties": {"properties": {"new": True}}}, "new" ) @@ -324,7 +323,7 @@ def test_validate_data_file_schema(self, mocker, paths): ) -class SchemaValidationErrorTest(CiTestCase): +class SchemaValidationErrorTest: """Test validate_cloudconfig_schema""" def test_schema_validation_error_expects_schema_errors(self): @@ -336,14 +335,14 @@ def test_schema_validation_error_expects_schema_errors(self): ), ] exception = SchemaValidationError(schema_errors=errors) - self.assertIsInstance(exception, Exception) - self.assertEqual(exception.schema_errors, errors) - self.assertEqual( + assert isinstance(exception, Exception) + assert exception.schema_errors == errors + assert ( 'Cloud config schema errors: key.path: unexpected key "junk", ' - 'key2.path: "-123" is not a valid "hostname" format', - str(exception), + 'key2.path: "-123" is not a valid "hostname" format' + == str(exception) ) - self.assertTrue(isinstance(exception, ValueError)) + assert isinstance(exception, ValueError) class FakeNetplanParserException(Exception): diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 75e7c1df34c..8510b04bb62 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -543,3 +543,13 @@ def get_distro(dname, system_info=None, /, renderers=None, activators=None): paths = helpers.Paths(system_info["paths"]) distro_cls = distros.fetch(dname) return distro_cls(dname, system_info, paths) + + +def assert_count_equal(a, b): + """ + Equivalent to unittests.TestCase.assertCountEqual. + + https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertCountEqual + """ + case = unittest.TestCase() + case.assertCountEqual(a, b) From 720d00b2b195549af978c139d70456ee1ec899c5 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Fri, 18 Jul 2025 13:31:17 +0200 Subject: [PATCH 033/150] test: drop citestcase from distro tests (#6317) --- tests/unittests/distros/test_aosc.py | 6 +- tests/unittests/distros/test_arch.py | 10 +-- tests/unittests/distros/test_azurelinux.py | 11 +-- tests/unittests/distros/test_bsd_utils.py | 82 +++++++------------ tests/unittests/distros/test_freebsd.py | 14 ++-- tests/unittests/distros/test_gentoo.py | 38 +++++---- .../unittests/distros/test_manage_service.py | 11 +-- tests/unittests/distros/test_mariner.py | 11 +-- tests/unittests/distros/test_photon.py | 37 ++++----- tests/unittests/distros/test_sles.py | 6 +- 10 files changed, 99 insertions(+), 127 deletions(-) diff --git a/tests/unittests/distros/test_aosc.py b/tests/unittests/distros/test_aosc.py index 6f4b0eb0eab..9bac093abcb 100644 --- a/tests/unittests/distros/test_aosc.py +++ b/tests/unittests/distros/test_aosc.py @@ -1,9 +1,9 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.helpers import CiTestCase, get_distro +from tests.unittests.helpers import get_distro -class TestAOSC(CiTestCase): +class TestAOSC: def test_get_distro(self): distro = get_distro("aosc") - self.assertEqual(distro.osfamily, "aosc") + assert distro.osfamily == "aosc" diff --git a/tests/unittests/distros/test_arch.py b/tests/unittests/distros/test_arch.py index d387bfd0682..962fa62ba76 100644 --- a/tests/unittests/distros/test_arch.py +++ b/tests/unittests/distros/test_arch.py @@ -1,13 +1,13 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import util -from tests.unittests.helpers import CiTestCase, get_distro +from tests.unittests.helpers import get_distro -class TestArch(CiTestCase): - def test_get_distro(self): +class TestArch: + def test_get_distro(self, tmp_path): distro = get_distro("arch") hostname = "myhostname" - hostfile = self.tmp_path("hostfile") + hostfile = tmp_path / "hostfile" distro._write_hostname(hostname, hostfile) - self.assertEqual(hostname + "\n", util.load_text_file(hostfile)) + assert hostname + "\n" == util.load_text_file(hostfile) diff --git a/tests/unittests/distros/test_azurelinux.py b/tests/unittests/distros/test_azurelinux.py index 519a8931698..f8134f3abf4 100644 --- a/tests/unittests/distros/test_azurelinux.py +++ b/tests/unittests/distros/test_azurelinux.py @@ -1,8 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.helpers import CiTestCase - -from ..helpers import get_distro +from tests.unittests.helpers import get_distro SYSTEM_INFO = { "paths": { @@ -13,13 +11,12 @@ } -class TestAzurelinux(CiTestCase): - with_logs = True +class TestAzurelinux: distro = get_distro("azurelinux", SYSTEM_INFO) expected_log_line = "Rely on Azure Linux default network config" def test_network_renderer(self): - self.assertEqual(self.distro._cfg["network"]["renderers"], "networkd") + assert self.distro._cfg["network"]["renderers"] == "networkd" def test_get_distro(self): - self.assertEqual(self.distro.osfamily, "azurelinux") + assert self.distro.osfamily == "azurelinux" diff --git a/tests/unittests/distros/test_bsd_utils.py b/tests/unittests/distros/test_bsd_utils.py index 285e0c3829c..f5a7efd8fc2 100644 --- a/tests/unittests/distros/test_bsd_utils.py +++ b/tests/unittests/distros/test_bsd_utils.py @@ -1,9 +1,8 @@ # This file is part of cloud-init. See LICENSE file for license information. - -from contextlib import ExitStack +import pytest import cloudinit.distros.bsd_utils as bsd_utils -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock RC_FILE = """ if something; then @@ -13,56 +12,37 @@ """ -class TestBsdUtils(CiTestCase): - def setUp(self): - super().setUp() - patches = ExitStack() - self.addCleanup(patches.close) - - self.load_file = patches.enter_context( - mock.patch.object(bsd_utils.util, "load_text_file") - ) - - self.write_file = patches.enter_context( - mock.patch.object(bsd_utils.util, "write_file") - ) - - def test_get_rc_config_value(self): - self.load_file.return_value = "hostname=foo\n" - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), "foo") - self.load_file.assert_called_with("/etc/rc.conf") - - self.load_file.return_value = "hostname=foo" - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), "foo") - - self.load_file.return_value = 'hostname="foo"' - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), "foo") - - self.load_file.return_value = "hostname='foo'" - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), "foo") - - self.load_file.return_value = "hostname='foo\"" - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), "'foo\"") - - self.load_file.return_value = "" - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), None) - - self.load_file.return_value = RC_FILE.format(hostname="foo") - self.assertEqual(bsd_utils.get_rc_config_value("hostname"), "foo") - - def test_set_rc_config_value_unchanged(self): - # bsd_utils.set_rc_config_value('hostname', 'foo') - # self.write_file.assert_called_with('/etc/rc.conf', 'hostname=foo\n') - - self.load_file.return_value = RC_FILE.format(hostname="foo") - self.write_file.assert_not_called() - - def test_set_rc_config_value(self): +@mock.patch("cloudinit.distros.bsd_utils.util.load_text_file") +class TestBsdUtils: + @pytest.mark.parametrize( + "content,expected", + ( + ("hostname=foo\n", "foo"), + ("hostname=foo", "foo"), + ('hostname="foo"', "foo"), + ("hostname='foo'", "foo"), + ("hostname='foo\"", "'foo\""), + ("", None), + (RC_FILE.format(hostname="foo"), "foo"), + ), + ) + def test_get_rc_config_value(self, m_load_file, content, expected): + m_load_file.return_value = content + assert bsd_utils.get_rc_config_value("hostname") == expected + m_load_file.assert_called_with("/etc/rc.conf") + + @mock.patch("cloudinit.distros.bsd_utils.util.write_file") + def test_set_rc_config_value_unchanged(self, m_write_file, m_load_file): + m_load_file.return_value = RC_FILE.format(hostname="foo") + m_write_file.assert_not_called() + + @mock.patch("cloudinit.distros.bsd_utils.util.write_file") + def test_set_rc_config_value(self, m_write_file, m_load_file): bsd_utils.set_rc_config_value("hostname", "foo") - self.write_file.assert_called_with("/etc/rc.conf", "hostname=foo\n") + m_write_file.assert_called_with("/etc/rc.conf", "hostname=foo\n") - self.load_file.return_value = RC_FILE.format(hostname="foo") + m_load_file.return_value = RC_FILE.format(hostname="foo") bsd_utils.set_rc_config_value("hostname", "bar") - self.write_file.assert_called_with( + m_write_file.assert_called_with( "/etc/rc.conf", RC_FILE.format(hostname="bar") ) diff --git a/tests/unittests/distros/test_freebsd.py b/tests/unittests/distros/test_freebsd.py index bda0f907d3b..ac1402bd134 100644 --- a/tests/unittests/distros/test_freebsd.py +++ b/tests/unittests/distros/test_freebsd.py @@ -3,7 +3,7 @@ import os from cloudinit.util import find_freebsd_part, get_path_dev_freebsd -from tests.unittests.helpers import CiTestCase, get_distro, mock +from tests.unittests.helpers import get_distro, mock M_PATH = "cloudinit.distros.freebsd." @@ -45,7 +45,7 @@ def test_unlock_passwd(self, caplog): ) -class TestDeviceLookUp(CiTestCase): +class TestDeviceLookUp: @mock.patch("cloudinit.subp.subp") def test_find_freebsd_part_label(self, mock_subp): glabel_out = """ @@ -55,7 +55,7 @@ def test_find_freebsd_part_label(self, mock_subp): """ mock_subp.return_value = (glabel_out, "") res = find_freebsd_part("/dev/label/rootfs") - self.assertEqual("da0p2", res) + assert "da0p2" == res @mock.patch("cloudinit.subp.subp") def test_find_freebsd_part_gpt(self, mock_subp): @@ -68,7 +68,7 @@ def test_find_freebsd_part_gpt(self, mock_subp): """ mock_subp.return_value = (glabel_out, "") res = find_freebsd_part("/dev/gpt/rootfs") - self.assertEqual("vtbd0p3", res) + assert "vtbd0p3" == res @mock.patch("cloudinit.subp.subp") def test_find_freebsd_part_gptid(self, mock_subp): @@ -82,7 +82,7 @@ def test_find_freebsd_part_gptid(self, mock_subp): res = find_freebsd_part( "/dev/gptid/4cd084b4-7fb4-11ee-a7ba-002590ec5bf2" ) - self.assertEqual("vtbd0p4", res) + assert "vtbd0p4" == res @mock.patch("cloudinit.subp.subp") def test_find_freebsd_part_ufsid(self, mock_subp): @@ -94,7 +94,7 @@ def test_find_freebsd_part_ufsid(self, mock_subp): """ mock_subp.return_value = (glabel_out, "") res = find_freebsd_part("/dev/ufsid/654e0663786f5131") - self.assertEqual("vtbd0p4", res) + assert "vtbd0p4" == res def test_get_path_dev_freebsd_label(self): mnt_list = """ @@ -105,4 +105,4 @@ def test_get_path_dev_freebsd_label(self): """ with mock.patch.object(os.path, "exists", return_value=True): res = get_path_dev_freebsd("/etc", mnt_list) - self.assertIsNotNone(res) + assert res is not None diff --git a/tests/unittests/distros/test_gentoo.py b/tests/unittests/distros/test_gentoo.py index 340a5cd4204..71916629ae4 100644 --- a/tests/unittests/distros/test_gentoo.py +++ b/tests/unittests/distros/test_gentoo.py @@ -1,41 +1,45 @@ # This file is part of cloud-init. See LICENSE file for license information. +import pytest from cloudinit import atomic_helper, util -from tests.unittests.helpers import CiTestCase, get_distro, mock +from tests.unittests.helpers import get_distro -class TestGentoo(CiTestCase): - def test_write_hostname(self, whatever=False): +class TestGentoo: + def test_write_hostname(self, tmp_path): distro = get_distro("gentoo") hostname = "myhostname" - hostfile = self.tmp_path("hostfile") + hostfile = tmp_path / "hostfile" distro._write_hostname(hostname, hostfile) if distro.uses_systemd(): - self.assertEqual("myhostname\n", util.load_text_file(hostfile)) + assert "myhostname\n" == util.load_text_file(hostfile) else: - self.assertEqual( - 'hostname="myhostname"\n', util.load_text_file(hostfile) - ) + assert 'hostname="myhostname"\n' == util.load_text_file(hostfile) - def test_write_existing_hostname_with_comments(self, whatever=False): + def test_write_existing_hostname_with_comments(self, tmp_path): distro = get_distro("gentoo") hostname = "myhostname" contents = '#This is the hostname\nhostname="localhost"' - hostfile = self.tmp_path("hostfile") + hostfile = tmp_path / "hostfile" atomic_helper.write_file(hostfile, contents, omode="w") distro._write_hostname(hostname, hostfile) if distro.uses_systemd(): - self.assertEqual( - "#This is the hostname\nmyhostname\n", - util.load_text_file(hostfile), + assert ( + "#This is the hostname\nmyhostname\n" + == util.load_text_file(hostfile) ) else: - self.assertEqual( - '#This is the hostname\nhostname="myhostname"\n', - util.load_text_file(hostfile), + assert ( + '#This is the hostname\nhostname="myhostname"\n' + == util.load_text_file(hostfile) ) -@mock.patch("cloudinit.distros.uses_systemd", return_value=False) +@pytest.fixture +def no_systemd(mocker): + mocker.patch("cloudinit.distros.uses_systemd", return_value=False) + + +@pytest.mark.usefixtures("no_systemd") class TestGentooOpenRC(TestGentoo): pass diff --git a/tests/unittests/distros/test_manage_service.py b/tests/unittests/distros/test_manage_service.py index 7a721d94842..8b87289796e 100644 --- a/tests/unittests/distros/test_manage_service.py +++ b/tests/unittests/distros/test_manage_service.py @@ -1,16 +1,11 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.helpers import CiTestCase, get_distro, mock +from tests.unittests.helpers import get_distro, mock from tests.unittests.util import MockDistro -class TestManageService(CiTestCase): - - with_logs = True - - def setUp(self): - super(TestManageService, self).setUp() - self.dist = MockDistro() +class TestManageService: + dist = MockDistro() @mock.patch.object(MockDistro, "uses_systemd", return_value=True) @mock.patch("cloudinit.distros.subp.subp") diff --git a/tests/unittests/distros/test_mariner.py b/tests/unittests/distros/test_mariner.py index 33243b64021..afda7d27445 100644 --- a/tests/unittests/distros/test_mariner.py +++ b/tests/unittests/distros/test_mariner.py @@ -1,8 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.helpers import CiTestCase - -from ..helpers import get_distro +from tests.unittests.helpers import get_distro SYSTEM_INFO = { "paths": { @@ -13,13 +11,12 @@ } -class TestMariner(CiTestCase): - with_logs = True +class TestMariner: distro = get_distro("mariner", SYSTEM_INFO) expected_log_line = "Rely on MarinerOS default network config" def test_network_renderer(self): - self.assertEqual(self.distro._cfg["network"]["renderers"], "networkd") + assert self.distro._cfg["network"]["renderers"] == "networkd" def test_get_distro(self): - self.assertEqual(self.distro.osfamily, "mariner") + assert self.distro.osfamily == "mariner" diff --git a/tests/unittests/distros/test_photon.py b/tests/unittests/distros/test_photon.py index 15b92d96a21..fdeade34523 100644 --- a/tests/unittests/distros/test_photon.py +++ b/tests/unittests/distros/test_photon.py @@ -1,7 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import util -from tests.unittests.helpers import CiTestCase, get_distro, mock +from tests.unittests.helpers import get_distro, mock SYSTEM_INFO = { "paths": { @@ -12,26 +12,25 @@ } -class TestPhoton(CiTestCase): - with_logs = True +class TestPhoton: distro = get_distro("photon", SYSTEM_INFO) expected_log_line = "Rely on PhotonOS default network config" def test_network_renderer(self): - self.assertEqual(self.distro._cfg["network"]["renderers"], "networkd") + assert self.distro._cfg["network"]["renderers"] == "networkd" def test_get_distro(self): - self.assertEqual(self.distro.osfamily, "photon") + assert self.distro.osfamily == "photon" @mock.patch("cloudinit.distros.photon.subp.subp") - def test_write_hostname(self, m_subp): + def test_write_hostname(self, m_subp, caplog, tmp_path): hostname = "myhostname" - hostfile = self.tmp_path("previous-hostname") + hostfile = str(tmp_path / "previous-hostname") self.distro._write_hostname(hostname, hostfile) - self.assertEqual(hostname, util.load_text_file(hostfile)) + assert hostname == util.load_text_file(hostfile) ret = self.distro._read_hostname(hostfile) - self.assertEqual(ret, hostname) + assert ret == hostname m_subp.return_value = (None, None) hostfile += "hostfile" @@ -39,28 +38,28 @@ def test_write_hostname(self, m_subp): m_subp.return_value = (hostname, None) ret = self.distro._read_hostname(hostfile) - self.assertEqual(ret, hostname) + assert ret == hostname - self.logs.truncate(0) + caplog.clear() m_subp.return_value = (None, "bla") self.distro._write_hostname(hostname, None) - self.assertIn("Error while setting hostname", self.logs.getvalue()) + assert "Error while setting hostname" in caplog.text @mock.patch("cloudinit.net.generate_fallback_config") - def test_fallback_netcfg(self, m_fallback_cfg): + def test_fallback_netcfg(self, m_fallback_cfg, caplog): key = "disable_fallback_netcfg" # Don't use fallback if no setting given - self.logs.truncate(0) + caplog.clear() assert self.distro.generate_fallback_config() is None - self.assertIn(self.expected_log_line, self.logs.getvalue()) + assert self.expected_log_line in caplog.text - self.logs.truncate(0) + caplog.clear() self.distro._cfg[key] = True assert self.distro.generate_fallback_config() is None - self.assertIn(self.expected_log_line, self.logs.getvalue()) + assert self.expected_log_line in caplog.text - self.logs.truncate(0) + caplog.clear() self.distro._cfg[key] = False assert self.distro.generate_fallback_config() is not None - self.assertNotIn(self.expected_log_line, self.logs.getvalue()) + assert self.expected_log_line not in caplog.text diff --git a/tests/unittests/distros/test_sles.py b/tests/unittests/distros/test_sles.py index c2f953cdbce..777e4267732 100644 --- a/tests/unittests/distros/test_sles.py +++ b/tests/unittests/distros/test_sles.py @@ -1,9 +1,9 @@ # This file is part of cloud-init. See LICENSE file for license information. -from tests.unittests.helpers import CiTestCase, get_distro +from tests.unittests.helpers import get_distro -class TestSLES(CiTestCase): +class TestSLES: def test_get_distro(self): distro = get_distro("sles") - self.assertEqual(distro.osfamily, "suse") + assert distro.osfamily == "suse" From c57cf3f15b36ff1a0d8294013652c21e18dde023 Mon Sep 17 00:00:00 2001 From: ludovictual-system-u Date: Fri, 18 Jul 2025 15:42:48 +0200 Subject: [PATCH 034/150] fix: GCE datasource query issue (#3398) (#6279) Fixes GH-3398 Co-authored-by: Alberto Contreras --- cloudinit/sources/DataSourceGCE.py | 6 ++++++ tests/unittests/sources/test_gce.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py index 8b5ddb5ff36..00a137df2e5 100644 --- a/cloudinit/sources/DataSourceGCE.py +++ b/cloudinit/sources/DataSourceGCE.py @@ -318,6 +318,12 @@ def read_md(address=None, url_params=None, platform_check=True): LOG.warning("unknown user-data-encoding: %s, ignoring", encoding) ret["user-data"] = ud + # Update md with parsed instance-data + md["instance-data"] = instance_data + + # Update md with parsed project-data + md["project-data"] = project_data + ret["meta-data"] = md ret["success"] = True diff --git a/tests/unittests/sources/test_gce.py b/tests/unittests/sources/test_gce.py index 41bc52dbcfb..729c12df0d7 100644 --- a/tests/unittests/sources/test_gce.py +++ b/tests/unittests/sources/test_gce.py @@ -543,3 +543,27 @@ def test_datasource_on_dhcp_lease_failure( msg, self.logs.getvalue(), ) + + @responses.activate + def test_instance_and_project_data_decoded(self): + """instance_data and project_data is decoded and can be queried.""" + md = { + "instance/id": "123", + "instance/zone": "foo/bar", + "instance/hostname": "server.project-foo.local", + "instance/attributes": {"ikey": "ivalue"}, + "project/attributes": {"pkey": "pvalue"}, + } + + self._set_mock_metadata(md) + assert self.ds.get_data() is True + + assert "instance-data" in self.ds.metadata + assert isinstance(self.ds.metadata["instance-data"], dict) + assert "ikey" in self.ds.metadata["instance-data"] + assert "ivalue" == self.ds.metadata["instance-data"]["ikey"] + + assert "project-data" in self.ds.metadata + assert isinstance(self.ds.metadata["project-data"], dict) + assert "pkey" in self.ds.metadata["project-data"] + assert "pvalue" == self.ds.metadata["project-data"]["pkey"] From 61f25bed0f1c41d36ccfa560e9dd64dfcda8effc Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 21 Jul 2025 16:14:20 +0200 Subject: [PATCH 035/150] test: drop citestcase from some config tests --- tests/unittests/config/test_cc_bootcmd.py | 69 ++--- .../config/test_cc_disable_ec2_metadata.py | 11 +- tests/unittests/config/test_cc_disk_setup.py | 20 +- tests/unittests/config/test_cc_puppet.py | 249 ++++++++-------- tests/unittests/config/test_cc_resizefs.py | 274 +++++++++--------- tests/unittests/conftest.py | 27 +- 6 files changed, 326 insertions(+), 324 deletions(-) diff --git a/tests/unittests/config/test_cc_bootcmd.py b/tests/unittests/config/test_cc_bootcmd.py index 29292405e54..46ca401ef95 100644 --- a/tests/unittests/config/test_cc_bootcmd.py +++ b/tests/unittests/config/test_cc_bootcmd.py @@ -13,7 +13,6 @@ ) from tests.unittests.helpers import ( SCHEMA_EMPTY_ERROR, - CiTestCase, mock, skipUnlessJsonSchema, ) @@ -35,72 +34,62 @@ def __exit__(self, exc_type, exc_value, traceback): util.del_file(self.handle.name) -class TestBootcmd(CiTestCase): - - with_logs = True +@pytest.mark.usefixtures("fake_filesystem") +class TestBootcmd: _etmpfile_path = ( "cloudinit.config.cc_bootcmd.temp_utils.ExtendedTemporaryFile" ) - def setUp(self): - super(TestBootcmd, self).setUp() - self.subp = subp.subp - self.new_root = self.tmp_dir() - - def test_handler_skip_if_no_bootcmd(self): + def test_handler_skip_if_no_bootcmd(self, caplog): """When the provided config doesn't contain bootcmd, skip it.""" cfg = {} mycloud = get_cloud() handle("notimportant", cfg, mycloud, None) - self.assertIn( - "Skipping module named notimportant, no 'bootcmd' key", - self.logs.getvalue(), + assert ( + "Skipping module named notimportant, no 'bootcmd' key" + in caplog.text ) - def test_handler_invalid_command_set(self): + def test_handler_invalid_command_set(self, caplog): """Commands which can't be converted to shell will raise errors.""" invalid_config = {"bootcmd": 1} cc = get_cloud() - with self.assertRaises(TypeError) as context_manager: + with pytest.raises( + TypeError, + match="Input to shellify was type 'int'. Expected list or tuple.", + ): handle("cc_bootcmd", invalid_config, cc, []) - self.assertIn("Failed to shellify bootcmd", self.logs.getvalue()) - self.assertEqual( - "Input to shellify was type 'int'. Expected list or tuple.", - str(context_manager.exception), - ) + assert "Failed to shellify bootcmd" in caplog.text invalid_config = { "bootcmd": ["ls /", 20, ["wget", "http://stuff/blah"], {"a": "n"}] } cc = get_cloud() - with self.assertRaises(TypeError) as context_manager: + with pytest.raises( + TypeError, + match="Unable to shellify type 'int'. Expected list, string, " + "tuple. Got: 20", + ): handle("cc_bootcmd", invalid_config, cc, []) - logs = self.logs.getvalue() - self.assertIn("Failed to shellify", logs) - self.assertEqual( - "Unable to shellify type 'int'. Expected list, string, tuple. " - "Got: 20", - str(context_manager.exception), - ) + assert "Failed to shellify" in caplog.text - def test_handler_runs_bootcmd_script_with_error(self): + @pytest.mark.allow_subp_for("/bin/sh") + def test_handler_runs_bootcmd_script_with_error(self, caplog): """When a valid script generates an error, that error is raised.""" cc = get_cloud() valid_config = {"bootcmd": ["exit 1"]} # Script with error with mock.patch(self._etmpfile_path, FakeExtendedTempFile): - with self.allow_subp(["/bin/sh"]): - with self.assertRaises(subp.ProcessExecutionError) as ctxt: - handle("does-not-matter", valid_config, cc, []) - self.assertIn( - "Unexpected error while running command.\nCommand: ['/bin/sh',", - str(ctxt.exception), - ) - self.assertIn( - "Failed to run bootcmd module does-not-matter", - self.logs.getvalue(), - ) + with pytest.raises( + subp.ProcessExecutionError, + match=( + r"Unexpected error while running command.\n" + r"Command: \['/bin/sh'," + ), + ): + handle("does-not-matter", valid_config, cc, []) + assert "Failed to run bootcmd module does-not-matter" in caplog.text @skipUnlessJsonSchema() diff --git a/tests/unittests/config/test_cc_disable_ec2_metadata.py b/tests/unittests/config/test_cc_disable_ec2_metadata.py index e67b07c25aa..c2ec97c0e55 100644 --- a/tests/unittests/config/test_cc_disable_ec2_metadata.py +++ b/tests/unittests/config/test_cc_disable_ec2_metadata.py @@ -11,12 +11,12 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema +from tests.unittests.helpers import mock, skipUnlessJsonSchema DISABLE_CFG = {"disable_ec2_metadata": "true"} -class TestEC2MetadataRoute(CiTestCase): +class TestEC2MetadataRoute: @mock.patch("cloudinit.config.cc_disable_ec2_metadata.subp.which") @mock.patch("cloudinit.config.cc_disable_ec2_metadata.subp.subp") def test_disable_ifconfig(self, m_subp, m_which): @@ -45,9 +45,10 @@ def test_disable_no_tool(self, m_subp, m_which): """Log error when neither route nor ip commands are available""" m_which.return_value = None # Find neither ifconfig nor ip ec2_meta.handle("foo", DISABLE_CFG, None, None) - self.assertEqual( - [mock.call("ip"), mock.call("ifconfig")], m_which.call_args_list - ) + assert [ + mock.call("ip"), + mock.call("ifconfig"), + ] == m_which.call_args_list m_subp.assert_not_called() diff --git a/tests/unittests/config/test_cc_disk_setup.py b/tests/unittests/config/test_cc_disk_setup.py index 1fab32df712..296c15d47af 100644 --- a/tests/unittests/config/test_cc_disk_setup.py +++ b/tests/unittests/config/test_cc_disk_setup.py @@ -13,7 +13,7 @@ get_schema, validate_cloudconfig_schema, ) -from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema +from tests.unittests.helpers import mock, skipUnlessJsonSchema class TestIsDiskUsed: @@ -194,10 +194,16 @@ def test_purge_disk_ptable(self, *args): ) @mock.patch("cloudinit.config.cc_disk_setup.device_type", return_value=None) @mock.patch("cloudinit.config.cc_disk_setup.subp.subp", return_value=("", "")) -class TestMkfsCommandHandling(CiTestCase): - with_logs = True - - def test_with_cmd(self, subp, *args): +class TestMkfsCommandHandling: + + def test_with_cmd( + self, + subp, + m_device_type, + m_find_device, + m_assert_and_settle_device, + caplog, + ): """mkfs honors cmd and logs warnings when extra_opts or overwrite are provided.""" cc_disk_setup.mkfs( @@ -214,12 +220,12 @@ def test_with_cmd(self, subp, *args): assert ( "extra_opts " "ignored because cmd was specified: mkfs -t ext4 -L with_cmd " - "/dev/xdb1" in self.logs.getvalue() + "/dev/xdb1" in caplog.text ) assert ( "overwrite " "ignored because cmd was specified: mkfs -t ext4 -L with_cmd " - "/dev/xdb1" in self.logs.getvalue() + "/dev/xdb1" in caplog.text ) subp.assert_called_once_with( diff --git a/tests/unittests/config/test_cc_puppet.py b/tests/unittests/config/test_cc_puppet.py index 47f27f93623..22911542d2a 100644 --- a/tests/unittests/config/test_cc_puppet.py +++ b/tests/unittests/config/test_cc_puppet.py @@ -13,7 +13,7 @@ ) from cloudinit.distros import PackageInstallerError from cloudinit.subp import ProcessExecutionError -from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema +from tests.unittests.helpers import mock, skipUnlessJsonSchema from tests.unittests.util import get_cloud @@ -25,16 +25,12 @@ def fake_tempdir(mocker, tmpdir): @mock.patch("cloudinit.config.cc_puppet.subp.subp") -class TestManagePuppetServices(CiTestCase): - def setUp(self): - super(TestManagePuppetServices, self).setUp() - self.cloud = get_cloud() - +class TestManagePuppetServices: def test_wb_manage_puppet_services_enables_puppet_systemctl( self, m_subp, ): - cc_puppet._manage_puppet_services(self.cloud, "enable") + cc_puppet._manage_puppet_services(get_cloud(), "enable") expected_calls = [ mock.call( ["systemctl", "enable", "puppet-agent.service"], @@ -42,13 +38,13 @@ def test_wb_manage_puppet_services_enables_puppet_systemctl( rcs=None, ) ] - self.assertIn(expected_calls, m_subp.call_args_list) + assert expected_calls in m_subp.call_args_list def test_wb_manage_puppet_services_starts_puppet_systemctl( self, m_subp, ): - cc_puppet._manage_puppet_services(self.cloud, "start") + cc_puppet._manage_puppet_services(get_cloud(), "start") expected_calls = [ mock.call( ["systemctl", "start", "puppet-agent.service"], @@ -56,11 +52,11 @@ def test_wb_manage_puppet_services_starts_puppet_systemctl( rcs=None, ) ] - self.assertIn(expected_calls, m_subp.call_args_list) + assert expected_calls in m_subp.call_args_list def test_enable_fallback_on_failure(self, m_subp): m_subp.side_effect = (ProcessExecutionError, 0) - cc_puppet._manage_puppet_services(self.cloud, "enable") + cc_puppet._manage_puppet_services(get_cloud(), "enable") expected_calls = [ mock.call( ["systemctl", "enable", "puppet-agent.service"], @@ -73,64 +69,60 @@ def test_enable_fallback_on_failure(self, m_subp): rcs=None, ), ] - self.assertEqual(expected_calls, m_subp.call_args_list) + assert expected_calls == m_subp.call_args_list +@pytest.mark.usefixtures("fake_filesystem") @mock.patch("cloudinit.config.cc_puppet._manage_puppet_services") -class TestPuppetHandle(CiTestCase): - with_logs = True - - def setUp(self): - super(TestPuppetHandle, self).setUp() - self.new_root = self.tmp_dir() - self.conf = self.tmp_path("puppet.conf") - self.csr_attributes_path = self.tmp_path("csr_attributes.yaml") - self.cloud = get_cloud() +class TestPuppetHandle: + CONF = "puppet.conf" + CSR_ATTRIBUTES_PATH = "csr_attributes.yaml" - def test_skips_missing_puppet_key_in_cloudconfig(self, m_man_puppet): + def test_skips_missing_puppet_key_in_cloudconfig( + self, m_man_puppet, caplog + ): """Cloud-config containing no 'puppet' key is skipped.""" cfg = {} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertIn("no 'puppet' configuration found", self.logs.getvalue()) - self.assertEqual(0, m_man_puppet.call_count) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + assert "no 'puppet' configuration found" in caplog.text + assert 0 == m_man_puppet.call_count @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_config_starts_puppet_service(self, m_subp, m_man_puppet): """Cloud-config 'puppet' configuration starts puppet.""" + cloud = get_cloud() cfg = {"puppet": {"install": False}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(2, m_man_puppet.call_count) + cc_puppet.handle("notimportant", cfg, cloud, None) + assert 2 == m_man_puppet.call_count expected_calls = [ - mock.call(self.cloud, "enable"), - mock.call(self.cloud, "start"), + mock.call(cloud, "enable"), + mock.call(cloud, "start"), ] - self.assertEqual(expected_calls, m_man_puppet.call_args_list) + assert expected_calls == m_man_puppet.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_empty_puppet_config_installs_puppet(self, m_subp, m_man_puppet): """Cloud-config empty 'puppet' configuration installs latest puppet.""" - - self.cloud.distro = mock.MagicMock() + cloud = get_cloud() + cloud.distro = mock.MagicMock() cfg = {"puppet": {}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual( - [mock.call(["puppet-agent"])], - self.cloud.distro.install_packages.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, cloud, None) + assert [ + mock.call(["puppet-agent"]) + ] == cloud.distro.install_packages.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_config_installs_puppet_on_true(self, m_subp, _): """Cloud-config with 'puppet' key installs when 'install' is True.""" - - self.cloud.distro = mock.MagicMock() + cloud = get_cloud() + cloud.distro = mock.MagicMock() cfg = {"puppet": {"install": True}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertIn( - [mock.call(["puppet-agent"])], - self.cloud.distro.install_packages.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, cloud, None) + assert [ + mock.call(["puppet-agent"]) + ] in cloud.distro.install_packages.call_args_list @mock.patch("cloudinit.config.cc_puppet.install_puppet_aio", autospec=True) @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) @@ -138,9 +130,10 @@ def test_puppet_config_installs_puppet_aio(self, m_subp, m_aio, _): """Cloud-config with 'puppet' key installs when 'install_type' is 'aio'.""" distro = mock.MagicMock() - self.cloud.distro = distro + cloud = get_cloud() + cloud.distro = distro cfg = {"puppet": {"install": True, "install_type": "aio"}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) + cc_puppet.handle("notimportant", cfg, cloud, None) m_aio.assert_called_with( distro, cc_puppet.AIO_INSTALL_URL, None, None, True ) @@ -153,7 +146,8 @@ def test_puppet_config_installs_puppet_aio_with_version( """Cloud-config with 'puppet' key installs when 'install_type' is 'aio' and 'version' is specified.""" distro = mock.MagicMock() - self.cloud.distro = distro + cloud = get_cloud() + cloud.distro = distro cfg = { "puppet": { "install": True, @@ -161,7 +155,7 @@ def test_puppet_config_installs_puppet_aio_with_version( "install_type": "aio", } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) + cc_puppet.handle("notimportant", cfg, cloud, None) m_aio.assert_called_with( distro, cc_puppet.AIO_INSTALL_URL, "6.24.0", None, True ) @@ -174,7 +168,8 @@ def test_puppet_config_installs_puppet_aio_with_collection( """Cloud-config with 'puppet' key installs when 'install_type' is 'aio' and 'collection' is specified.""" distro = mock.MagicMock() - self.cloud.distro = distro + cloud = get_cloud() + cloud.distro = distro cfg = { "puppet": { "install": True, @@ -182,7 +177,7 @@ def test_puppet_config_installs_puppet_aio_with_collection( "install_type": "aio", } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) + cc_puppet.handle("notimportant", cfg, cloud, None) m_aio.assert_called_with( distro, cc_puppet.AIO_INSTALL_URL, None, "puppet6", True ) @@ -195,7 +190,8 @@ def test_puppet_config_installs_puppet_aio_with_custom_url( """Cloud-config with 'puppet' key installs when 'install_type' is 'aio' and 'aio_install_url' is specified.""" distro = mock.MagicMock() - self.cloud.distro = distro + cloud = get_cloud() + cloud.distro = distro cfg = { "puppet": { "install": True, @@ -203,7 +199,7 @@ def test_puppet_config_installs_puppet_aio_with_custom_url( "install_type": "aio", } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) + cc_puppet.handle("notimportant", cfg, cloud, None) m_aio.assert_called_with( distro, "http://test.url/path/to/script.sh", None, None, True ) @@ -216,7 +212,8 @@ def test_puppet_config_installs_puppet_aio_without_cleanup( """Cloud-config with 'puppet' key installs when 'install_type' is 'aio' and no cleanup.""" distro = mock.MagicMock() - self.cloud.distro = distro + cloud = get_cloud() + cloud.distro = distro cfg = { "puppet": { "install": True, @@ -224,7 +221,7 @@ def test_puppet_config_installs_puppet_aio_without_cleanup( "install_type": "aio", } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) + cc_puppet.handle("notimportant", cfg, cloud, None) m_aio.assert_called_with( distro, cc_puppet.AIO_INSTALL_URL, None, None, False ) @@ -232,14 +229,13 @@ def test_puppet_config_installs_puppet_aio_without_cleanup( @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_config_installs_puppet_version(self, m_subp, _): """Cloud-config 'puppet' configuration can specify a version.""" - - self.cloud.distro = mock.MagicMock() + cloud = get_cloud() + cloud.distro = mock.MagicMock() cfg = {"puppet": {"version": "3.8"}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual( - [mock.call([["puppet-agent", "3.8"]])], - self.cloud.distro.install_packages.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, cloud, None) + assert [ + mock.call([["puppet-agent", "3.8"]]) + ] == cloud.distro.install_packages.call_args_list @mock.patch("cloudinit.config.cc_puppet.get_config_value") @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) @@ -249,7 +245,7 @@ def test_puppet_config_updates_puppet_conf( """When 'conf' is provided update values in PUPPET_CONF_PATH.""" def _fake_get_config_value(puppet_bin, setting): - return self.conf + return self.CONF m_default.side_effect = _fake_get_config_value @@ -258,12 +254,13 @@ def _fake_get_config_value(puppet_bin, setting): "conf": {"agent": {"server": "puppetserver.example.org"}} } } - util.write_file(self.conf, "[agent]\nserver = origpuppet\nother = 3") - self.cloud.distro = mock.MagicMock() - cc_puppet.handle("notimportant", cfg, self.cloud, None) - content = util.load_text_file(self.conf) + util.write_file(self.CONF, "[agent]\nserver = origpuppet\nother = 3") + cloud = get_cloud() + cloud.distro = mock.MagicMock() + cc_puppet.handle("notimportant", cfg, cloud, None) + content = util.load_text_file(self.CONF) expected = "[agent]\nserver = puppetserver.example.org\nother = 3\n\n" - self.assertEqual(expected, content) + assert expected == content @mock.patch("cloudinit.config.cc_puppet.get_config_value") @mock.patch("cloudinit.config.cc_puppet.subp.subp") @@ -274,11 +271,11 @@ def test_puppet_writes_csr_attributes_file( creates file in PUPPET_CSR_ATTRIBUTES_PATH.""" def _fake_get_config_value(puppet_bin, setting): - return self.csr_attributes_path + return self.CSR_ATTRIBUTES_PATH m_default.side_effect = _fake_get_config_value - self.cloud.distro = mock.MagicMock() + get_cloud().distro = mock.MagicMock() cfg = { "puppet": { "csr_attributes": { @@ -297,8 +294,8 @@ def _fake_get_config_value(puppet_bin, setting): } } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) - content = util.load_text_file(self.csr_attributes_path) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + content = util.load_text_file(self.CSR_ATTRIBUTES_PATH) expected = textwrap.dedent( """\ custom_attributes: @@ -309,49 +306,47 @@ def _fake_get_config_value(puppet_bin, setting): pp_uuid: ED803750-E3C7-44F5-BB08-41A04433FE2E """ ) - self.assertEqual(expected, content) + assert expected == content @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_runs_puppet_if_requested(self, m_subp, m_man_puppet): """Run puppet with default args if 'exec' is set to True.""" - + cloud = get_cloud() cfg = {"puppet": {"exec": True}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(2, m_man_puppet.call_count) + cc_puppet.handle("notimportant", cfg, cloud, None) + assert 2 == m_man_puppet.call_count expected_calls = [ - mock.call(self.cloud, "enable"), - mock.call(self.cloud, "start"), + mock.call(cloud, "enable"), + mock.call(cloud, "start"), ] - self.assertEqual(expected_calls, m_man_puppet.call_args_list) - self.assertIn( - [mock.call(["puppet", "agent", "--test"], capture=False)], - m_subp.call_args_list, - ) + assert expected_calls == m_man_puppet.call_args_list + assert [ + mock.call(["puppet", "agent", "--test"], capture=False) + ] in m_subp.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_starts_puppetd(self, m_subp, m_man_puppet): """Run puppet with default args if 'exec' is set to True.""" - + cloud = get_cloud() cfg = {"puppet": {}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(2, m_man_puppet.call_count) + cc_puppet.handle("notimportant", cfg, cloud, None) + assert 2 == m_man_puppet.call_count expected_calls = [ - mock.call(self.cloud, "enable"), - mock.call(self.cloud, "start"), + mock.call(cloud, "enable"), + mock.call(cloud, "start"), ] - self.assertEqual(expected_calls, m_man_puppet.call_args_list) + assert expected_calls == m_man_puppet.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_skips_puppetd(self, m_subp, m_man_puppet): """Run puppet with default args if 'exec' is set to True.""" cfg = {"puppet": {"start_service": False}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(0, m_man_puppet.call_count) - self.assertNotIn( - [mock.call(["systemctl", "start", "puppet-agent"], capture=False)], - m_subp.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + assert 0 == m_man_puppet.call_count + assert [ + mock.call(["systemctl", "start", "puppet-agent"], capture=False) + ] not in m_subp.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_runs_puppet_with_args_list_if_requested( @@ -365,17 +360,14 @@ def test_puppet_runs_puppet_with_args_list_if_requested( "exec_args": ["--onetime", "--detailed-exitcodes"], } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(2, m_man_puppet.call_count) - self.assertIn( - [ - mock.call( - ["puppet", "agent", "--onetime", "--detailed-exitcodes"], - capture=False, - ) - ], - m_subp.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + assert 2 == m_man_puppet.call_count + assert [ + mock.call( + ["puppet", "agent", "--onetime", "--detailed-exitcodes"], + capture=False, + ) + ] in m_subp.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_runs_puppet_with_args_string_if_requested( @@ -389,17 +381,14 @@ def test_puppet_runs_puppet_with_args_string_if_requested( "exec_args": "--onetime --detailed-exitcodes", } } - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(2, m_man_puppet.call_count) - self.assertIn( - [ - mock.call( - ["puppet", "agent", "--onetime", "--detailed-exitcodes"], - capture=False, - ) - ], - m_subp.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + assert 2 == m_man_puppet.call_count + assert [ + mock.call( + ["puppet", "agent", "--onetime", "--detailed-exitcodes"], + capture=False, + ) + ] in m_subp.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_falls_back_to_older_name(self, m_subp, m_man_puppet): @@ -410,12 +399,13 @@ def test_puppet_falls_back_to_older_name(self, m_subp, m_man_puppet): # puppet-agent not installed, but puppet is install_pkg.side_effect = (PackageInstallerError, 0) - cc_puppet.handle("notimportant", cfg, self.cloud, None) + cloud = get_cloud() + cc_puppet.handle("notimportant", cfg, cloud, None) expected_calls = [ - mock.call(self.cloud, "enable"), - mock.call(self.cloud, "start"), + mock.call(cloud, "enable"), + mock.call(cloud, "start"), ] - self.assertEqual(expected_calls, m_man_puppet.call_args_list) + assert expected_calls == m_man_puppet.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_with_conf_package_name_fails(self, m_subp, m_man_puppet): @@ -426,22 +416,17 @@ def test_puppet_with_conf_package_name_fails(self, m_subp, m_man_puppet): # puppet-agent not installed, but puppet is install_pkg.side_effect = (ProcessExecutionError, 0) with pytest.raises(ProcessExecutionError): - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(0, m_man_puppet.call_count) - self.assertNotIn( - [ - mock.call( - ["systemctl", "start", "puppet-agent"], capture=True - ) - ], - m_subp.call_args_list, - ) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + assert 0 == m_man_puppet.call_count + assert [ + mock.call(["systemctl", "start", "puppet-agent"], capture=True) + ] not in m_subp.call_args_list @mock.patch("cloudinit.config.cc_puppet.subp.subp", return_value=("", "")) def test_puppet_with_conf_package_name_success(self, m_subp, m_man_puppet): cfg = {"puppet": {"package_name": "puppet"}} - cc_puppet.handle("notimportant", cfg, self.cloud, None) - self.assertEqual(2, m_man_puppet.call_count) + cc_puppet.handle("notimportant", cfg, get_cloud(), None) + assert 2 == m_man_puppet.call_count URL_MOCK = mock.Mock() diff --git a/tests/unittests/config/test_cc_resizefs.py b/tests/unittests/config/test_cc_resizefs.py index 8648f085629..612f76f772d 100644 --- a/tests/unittests/config/test_cc_resizefs.py +++ b/tests/unittests/config/test_cc_resizefs.py @@ -24,7 +24,6 @@ ) from cloudinit.subp import ProcessExecutionError, SubpResult from tests.unittests.helpers import ( - CiTestCase, mock, readResource, skipUnlessJsonSchema, @@ -36,9 +35,7 @@ M_PATH = "cloudinit.config.cc_resizefs." -class TestResizefs(CiTestCase): - with_logs = True - +class TestResizefs: def setUp(self): super(TestResizefs, self).setUp() self.name = "resizefs" @@ -55,7 +52,7 @@ def test_skip_ufs_resize(self, m_subp): exception = ProcessExecutionError(stderr=err, exit_code=1) m_subp.side_effect = exception res = can_skip_resize(fs_type, resize_what, devpth) - self.assertTrue(res) + assert res @mock.patch("cloudinit.subp.subp") def test_cannot_skip_ufs_resize(self, m_subp): @@ -68,7 +65,7 @@ def test_cannot_skip_ufs_resize(self, m_subp): "leaving 364KB unused\n", ) res = can_skip_resize(fs_type, resize_what, devpth) - self.assertFalse(res) + assert not res @mock.patch("cloudinit.subp.subp") def test_cannot_skip_ufs_growfs_exception(self, m_subp): @@ -78,39 +75,41 @@ def test_cannot_skip_ufs_growfs_exception(self, m_subp): err = "growfs: /dev/da0p2 is not clean - run fsck.\n" exception = ProcessExecutionError(stderr=err, exit_code=1) m_subp.side_effect = exception - with self.assertRaises(ProcessExecutionError): + with pytest.raises(ProcessExecutionError): can_skip_resize(fs_type, resize_what, devpth) def test_can_skip_resize_ext(self): - self.assertFalse(can_skip_resize("ext", "/", "/dev/sda1")) + assert not can_skip_resize("ext", "/", "/dev/sda1") - def test_handle_noops_on_disabled(self): + def test_handle_noops_on_disabled(self, caplog): """The handle function logs when the configuration disables resize.""" cfg = {"resize_rootfs": False} handle("cc_resizefs", cfg, cloud=None, args=[]) - self.assertIn( - "DEBUG: Skipping module named cc_resizefs, resizing disabled\n", - self.logs.getvalue(), - ) + assert ( + mock.ANY, + logging.DEBUG, + "Skipping module named cc_resizefs, resizing disabled", + ) in caplog.record_tuples @mock.patch("cloudinit.config.cc_resizefs.util.get_mount_info") @mock.patch("cloudinit.config.cc_resizefs.LOG") - def test_handle_warns_on_unknown_mount_info(self, m_log, m_get_mount_info): + def test_handle_warns_on_unknown_mount_info( + self, m_log, m_get_mount_info, caplog + ): """handle warns when get_mount_info sees unknown filesystem for /.""" m_get_mount_info.return_value = None cfg = {"resize_rootfs": True} handle("cc_resizefs", cfg, cloud=None, args=[]) - logs = self.logs.getvalue() - self.assertNotIn( - "WARNING: Invalid cloud-config provided:\nresize_rootfs:", logs - ) - self.assertEqual( - ("Could not determine filesystem type of %s", "/"), - m_log.warning.call_args[0], - ) - self.assertEqual( - [mock.call("/", m_log)], m_get_mount_info.call_args_list + logs = caplog.text + assert ( + "WARNING: Invalid cloud-config provided:\nresize_rootfs:" + not in logs ) + assert ( + "Could not determine filesystem type of %s", + "/", + ) == m_log.warning.call_args[0] + assert [mock.call("/", m_log)] == m_get_mount_info.call_args_list @mock.patch("cloudinit.config.cc_resizefs.LOG") def test_handle_warns_on_undiscoverable_root_path_in_command_line( @@ -121,8 +120,8 @@ def test_handle_warns_on_undiscoverable_root_path_in_command_line( exists_mock_path = "cloudinit.config.cc_resizefs.os.path.exists" def fake_mount_info(path, log): - self.assertEqual("/", path) - self.assertEqual(m_log, log) + assert "/" == path + assert m_log == log return ("/dev/root", "ext4", "/") with mock.patch(exists_mock_path) as m_exists: @@ -140,45 +139,39 @@ def fake_mount_info(path, log): cloud=None, args=[], ) - self.assertIn( - "Unable to find device '/dev/root'", m_log.warning.call_args[0] + assert ( + "Unable to find device '/dev/root'" in m_log.warning.call_args[0] ) def test_resize_zfs_cmd_return(self): zpool = "zroot" devpth = "gpt/system" - self.assertEqual( - ("zpool", "online", "-e", zpool, devpth), - _resize_zfs(zpool, devpth), + assert ("zpool", "online", "-e", zpool, devpth) == _resize_zfs( + zpool, devpth ) def test_resize_xfs_cmd_return(self): mount_point = "/mnt/test" devpth = "/dev/sda1" - self.assertEqual( - ("xfs_growfs", mount_point), _resize_xfs(mount_point, devpth) - ) + assert ("xfs_growfs", mount_point) == _resize_xfs(mount_point, devpth) def test_resize_ext_cmd_return(self): mount_point = "/" devpth = "/dev/sdb1" - self.assertEqual( - ("resize2fs", devpth), _resize_ext(mount_point, devpth) - ) + assert ("resize2fs", devpth) == _resize_ext(mount_point, devpth) def test_resize_ufs_cmd_return(self): mount_point = "/" devpth = "/dev/sda2" - self.assertEqual( - ("growfs", "-y", mount_point), _resize_ufs(mount_point, devpth) + assert ("growfs", "-y", mount_point) == _resize_ufs( + mount_point, devpth ) def test_resize_bcachefs_cmd_return(self): mount_point = "/" devpth = "/dev/sdf3" - self.assertEqual( - ("bcachefs", "device", "resize", devpth), - _resize_bcachefs(mount_point, devpth), + assert ("bcachefs", "device", "resize", devpth) == _resize_bcachefs( + mount_point, devpth ) @mock.patch("cloudinit.util.is_container", return_value=False) @@ -203,7 +196,7 @@ def test_handle_zfs_root( handle("cc_resizefs", cfg, cloud=None, args=[]) ret = dresize.call_args[0] - self.assertEqual((("zpool", "online", "-e", "vmzroot", disk),), ret) + assert (("zpool", "online", "-e", "vmzroot", disk),) == ret @mock.patch("cloudinit.util.is_container", return_value=False) @mock.patch("cloudinit.util.get_mount_info") @@ -235,13 +228,12 @@ def fake_stat(devpath): with mock.patch("cloudinit.config.cc_resizefs.os.stat") as m_stat: m_stat.side_effect = fake_stat handle("cc_resizefs", cfg, cloud=None, args=[]) - self.assertEqual( - (("zpool", "online", "-e", "zroot", "/dev/" + disk),), - dresize.call_args[0], - ) + assert ( + ("zpool", "online", "-e", "zroot", "/dev/" + disk), + ) == dresize.call_args[0] -class TestRootDevFromCmdline(CiTestCase): +class TestRootDevFromCmdline: def test_rootdev_from_cmdline_with_no_root(self): """Return None from rootdev_from_cmdline when root is not present.""" invalid_cases = [ @@ -250,40 +242,32 @@ def test_rootdev_from_cmdline_with_no_root(self): "", ] for case in invalid_cases: - self.assertIsNone(util.rootdev_from_cmdline(case)) + assert util.rootdev_from_cmdline(case) is None def test_rootdev_from_cmdline_with_root_startswith_dev(self): """Return the cmdline root when the path starts with /dev.""" - self.assertEqual( - "/dev/this", util.rootdev_from_cmdline("asdf root=/dev/this") - ) + assert "/dev/this" == util.rootdev_from_cmdline("asdf root=/dev/this") def test_rootdev_from_cmdline_with_root_without_dev_prefix(self): """Add /dev prefix to cmdline root when the path lacks the prefix.""" - self.assertEqual( - "/dev/this", util.rootdev_from_cmdline("asdf root=this") - ) + assert "/dev/this" == util.rootdev_from_cmdline("asdf root=this") def test_rootdev_from_cmdline_with_root_with_label(self): """When cmdline root contains a LABEL, our root is disk/by-label.""" - self.assertEqual( - "/dev/disk/by-label/unique", - util.rootdev_from_cmdline("asdf root=LABEL=unique"), + assert "/dev/disk/by-label/unique" == util.rootdev_from_cmdline( + "asdf root=LABEL=unique" ) def test_rootdev_from_cmdline_with_root_with_uuid(self): """When cmdline root contains a UUID, our root is disk/by-uuid.""" - self.assertEqual( - "/dev/disk/by-uuid/adsfdsaf-adsf", - util.rootdev_from_cmdline("asdf root=UUID=adsfdsaf-adsf"), + assert "/dev/disk/by-uuid/adsfdsaf-adsf" == util.rootdev_from_cmdline( + "asdf root=UUID=adsfdsaf-adsf" ) -class TestMaybeGetDevicePathAsWritableBlock(CiTestCase): - - with_logs = True - - def test_maybe_get_writable_device_path_none_on_overlayroot(self): +@pytest.mark.usefixtures("fake_filesystem") +class TestMaybeGetDevicePathAsWritableBlock: + def test_maybe_get_writable_device_path_none_on_overlayroot(self, caplog): """When devpath is overlayroot (on MAAS), is_dev_writable is False.""" info = "does not matter" devpath = wrap_and_call( @@ -293,19 +277,18 @@ def test_maybe_get_writable_device_path_none_on_overlayroot(self): "overlayroot", info, ) - self.assertIsNone(devpath) - self.assertIn( - "Not attempting to resize devpath 'overlayroot'", - self.logs.getvalue(), - ) + assert devpath is None + assert "Not attempting to resize devpath 'overlayroot'" in caplog.text - def test_maybe_get_writable_device_path_warns_missing_cmdline_root(self): + def test_maybe_get_writable_device_path_warns_missing_cmdline_root( + self, caplog + ): """When root does not exist isn't in the cmdline, log warning.""" info = "does not matter" def fake_mount_info(path, log): - self.assertEqual("/", path) - self.assertEqual(LOG, log) + assert "/" == path + assert LOG == log return ("/dev/root", "ext4", "/") exists_mock_path = "cloudinit.config.cc_resizefs.os.path.exists" @@ -322,11 +305,14 @@ def fake_mount_info(path, log): "/dev/root", info, ) - self.assertIsNone(devpath) - logs = self.logs.getvalue() - self.assertIn("WARNING: Unable to find device '/dev/root'", logs) - - def test_maybe_get_writable_device_path_does_not_exist(self): + assert devpath is None + assert ( + mock.ANY, + logging.WARNING, + "Unable to find device '/dev/root'", + ) in caplog.record_tuples + + def test_maybe_get_writable_device_path_does_not_exist(self, caplog): """When devpath does not exist, a warning is logged.""" info = "dev=/dev/I/dont/exist mnt_point=/ path=/dev/none" devpath = wrap_and_call( @@ -336,14 +322,17 @@ def test_maybe_get_writable_device_path_does_not_exist(self): "/dev/I/dont/exist", info, ) - self.assertIsNone(devpath) - self.assertIn( - "WARNING: Device '/dev/I/dont/exist' did not exist." - " cannot resize: %s" % info, - self.logs.getvalue(), - ) + assert devpath is None + assert ( + mock.ANY, + logging.WARNING, + "Device '/dev/I/dont/exist' did not exist. cannot resize: %s" + % info, + ) in caplog.record_tuples - def test_maybe_get_writable_device_path_does_not_exist_in_container(self): + def test_maybe_get_writable_device_path_does_not_exist_in_container( + self, caplog + ): """When devpath does not exist in a container, log a debug message.""" info = "dev=/dev/I/dont/exist mnt_point=/ path=/dev/none" devpath = wrap_and_call( @@ -353,17 +342,18 @@ def test_maybe_get_writable_device_path_does_not_exist_in_container(self): "/dev/I/dont/exist", info, ) - self.assertIsNone(devpath) - self.assertIn( - "DEBUG: Device '/dev/I/dont/exist' did not exist in container." - " cannot resize: %s" % info, - self.logs.getvalue(), - ) + assert devpath is None + assert ( + mock.ANY, + logging.DEBUG, + "Device '/dev/I/dont/exist' did not exist in container. cannot" + " resize: %s" % info, + ) in caplog.record_tuples def test_maybe_get_writable_device_path_raises_oserror(self): """When unexpected OSError is raises by os.stat it is reraised.""" info = "dev=/dev/I/dont/exist mnt_point=/ path=/dev/none" - with self.assertRaises(OSError) as context_manager: + with pytest.raises(OSError, match="Something unexpected"): wrap_and_call( "cloudinit.config.cc_resizefs", { @@ -376,13 +366,10 @@ def test_maybe_get_writable_device_path_raises_oserror(self): "/dev/I/dont/exist", info, ) - self.assertEqual( - "Something unexpected", str(context_manager.exception) - ) - def test_maybe_get_writable_device_path_non_block(self): + def test_maybe_get_writable_device_path_non_block(self, caplog): """When device is not a block device, emit warning return False.""" - fake_devpath = self.tmp_path("dev/readwrite") + fake_devpath = "dev/readwrite" util.write_file(fake_devpath, "", mode=0o600) # read-write info = "dev=/dev/root mnt_point=/ path={0}".format(fake_devpath) @@ -393,17 +380,20 @@ def test_maybe_get_writable_device_path_non_block(self): fake_devpath, info, ) - self.assertIsNone(devpath) - self.assertIn( - "WARNING: device '{0}' not a block device. cannot resize".format( - fake_devpath + assert devpath is None + assert ( + mock.ANY, + logging.WARNING, + "device '{0}' not a block device. cannot resize: {1}".format( + fake_devpath, info ), - self.logs.getvalue(), - ) + ) in caplog.record_tuples - def test_maybe_get_writable_device_path_non_block_on_container(self): + def test_maybe_get_writable_device_path_non_block_on_container( + self, caplog + ): """When device is non-block device in container, emit debug log.""" - fake_devpath = self.tmp_path("dev/readwrite") + fake_devpath = "dev/readwrite" util.write_file(fake_devpath, "", mode=0o600) # read-write info = "dev=/dev/root mnt_point=/ path={0}".format(fake_devpath) @@ -414,14 +404,17 @@ def test_maybe_get_writable_device_path_non_block_on_container(self): fake_devpath, info, ) - self.assertIsNone(devpath) - self.assertIn( - "DEBUG: device '{0}' not a block device in container." - " cannot resize".format(fake_devpath), - self.logs.getvalue(), - ) + assert devpath is None + assert ( + mock.ANY, + logging.DEBUG, + "device '{0}' not a block device in container. cannot resize:" + " {1}".format(fake_devpath, info), + ) in caplog.record_tuples - def test_maybe_get_writable_device_path_returns_command_line_root(self): + def test_maybe_get_writable_device_path_returns_command_line_root( + self, caplog + ): """When root device is UUID in kernel command_line, update devpath.""" # XXX Long-term we want to use fake_filesystem test to avoid # touching os.stat. @@ -443,12 +436,13 @@ def test_maybe_get_writable_device_path_returns_command_line_root(self): "/dev/root", info, ) - self.assertEqual("/dev/disk/by-uuid/my-uuid", devpath) - self.assertIn( - "DEBUG: Converted /dev/root to '/dev/disk/by-uuid/my-uuid'" - " per kernel cmdline", - self.logs.getvalue(), - ) + assert "/dev/disk/by-uuid/my-uuid" == devpath + assert ( + mock.ANY, + logging.DEBUG, + "Converted /dev/root to '/dev/disk/by-uuid/my-uuid' per kernel" + " cmdline", + ) in caplog.record_tuples @mock.patch("cloudinit.util.mount_is_read_write") @mock.patch("cloudinit.config.cc_resizefs.os.path.isdir") @@ -458,10 +452,13 @@ def test_resize_btrfs_mount_is_ro(self, m_subp, m_is_dir, m_is_rw): m_is_rw.return_value = False m_is_dir.return_value = True m_subp.return_value = SubpResult("btrfs-progs v4.19 \n", "") - self.assertEqual( - ("btrfs", "filesystem", "resize", "max", "//.snapshots"), - _resize_btrfs("/", "/dev/sda1"), - ) + assert ( + "btrfs", + "filesystem", + "resize", + "max", + "//.snapshots", + ) == _resize_btrfs("/", "/dev/sda1") @mock.patch("cloudinit.util.mount_is_read_write") @mock.patch("cloudinit.config.cc_resizefs.os.path.isdir") @@ -471,9 +468,8 @@ def test_resize_btrfs_mount_is_rw(self, m_subp, m_is_dir, m_is_rw): m_is_rw.return_value = True m_is_dir.return_value = True m_subp.return_value = SubpResult("btrfs-progs v4.19 \n", "") - self.assertEqual( - ("btrfs", "filesystem", "resize", "max", "/"), - _resize_btrfs("/", "/dev/sda1"), + assert ("btrfs", "filesystem", "resize", "max", "/") == _resize_btrfs( + "/", "/dev/sda1" ) @mock.patch("cloudinit.util.mount_is_read_write") @@ -486,10 +482,14 @@ def test_resize_btrfs_mount_is_rw_has_queue( m_is_rw.return_value = True m_is_dir.return_value = True m_subp.return_value = SubpResult("btrfs-progs v5.10 \n", "") - self.assertEqual( - ("btrfs", "filesystem", "resize", "--enqueue", "max", "/"), - _resize_btrfs("/", "/dev/sda1"), - ) + assert ( + "btrfs", + "filesystem", + "resize", + "--enqueue", + "max", + "/", + ) == _resize_btrfs("/", "/dev/sda1") @mock.patch("cloudinit.util.mount_is_read_write") @mock.patch("cloudinit.config.cc_resizefs.os.path.isdir") @@ -503,10 +503,14 @@ def test_resize_btrfs_version(self, m_subp, m_is_dir, m_is_rw): "+UDEV +FSVERITY +ZONED CRYPTO=libgcrypt", "", ) - self.assertEqual( - ("btrfs", "filesystem", "resize", "--enqueue", "max", "/"), - _resize_btrfs("/", "/dev/sda1"), - ) + assert ( + "btrfs", + "filesystem", + "resize", + "--enqueue", + "max", + "/", + ) == _resize_btrfs("/", "/dev/sda1") @mock.patch("cloudinit.util.is_container", return_value=True) @mock.patch("cloudinit.util.is_FreeBSD") @@ -516,7 +520,7 @@ def test_maybe_get_writable_device_path_zfs_freebsd( freebsd.return_value = True info = "dev=gpt/system mnt_point=/ path=/" devpth = maybe_get_writable_device_path("gpt/system", info) - self.assertEqual("gpt/system", devpth) + assert "gpt/system" == devpth class TestResizefsSchema: diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index af6e42488fa..dbae8277e07 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -7,13 +7,17 @@ import pytest -from cloudinit import atomic_helper, distros, helpers, lifecycle +from cloudinit import atomic_helper, distros, helpers, lifecycle, temp_utils from cloudinit import user_data as ud from cloudinit import util from cloudinit.gpg import GPG from cloudinit.log import loggers from tests.hypothesis import HAS_HYPOTHESIS -from tests.unittests.helpers import example_netdev, retarget_many_wrapper +from tests.unittests.helpers import ( + example_netdev, + rebase_path, + retarget_many_wrapper, +) @pytest.fixture @@ -50,14 +54,16 @@ def m_gpg(): ("relpath", 1), ], os: [ + ("chmod", 2), ("chown", 2), ("listdir", 1), + ("lstat", 1), ("mkdir", 1), + ("rename", 2), ("rmdir", 1), - ("lstat", 1), - ("symlink", 2), - ("stat", 1), ("scandir", 1), + ("stat", 1), + ("symlink", 2), ], util: [ ("write_file", 1), @@ -86,6 +92,10 @@ def m_gpg(): ], } +FS_VARS = { + temp_utils: ["_ROOT_TMPDIR", "_EXE_ROOT_TMPDIR"], +} + @pytest.fixture def fake_filesystem_hook(): @@ -118,6 +128,13 @@ def fake_filesystem(mocker, tmpdir, fake_filesystem_hook): func = getattr(mod, f) trap_func = retarget_many_wrapper(str(tmpdir), nargs, func) mocker.patch.object(mod, f, trap_func) + + for mod, vars in FS_VARS.items(): + for var_name in vars: + var_val = getattr(mod, var_name) + new_var_val = rebase_path(var_val, str(tmpdir)) + mocker.patch.object(mod, var_name, new_var_val) + yield str(tmpdir) From 067197e3301271bdc20a9613840289f56ad4be1a Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab <46541787+mostafaCamel@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:39:34 +0300 Subject: [PATCH 036/150] fix: catch and log exception during pip upgrade in ansible pull (#6301) Fixes GH-6074 Signed-off-by: Mostafa Abdelwahab --- cloudinit/config/cc_ansible.py | 21 +++++++- tests/unittests/config/test_cc_ansible.py | 61 +++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/cloudinit/config/cc_ansible.py b/cloudinit/config/cc_ansible.py index 342c80240bb..66c6f425e50 100644 --- a/cloudinit/config/cc_ansible.py +++ b/cloudinit/config/cc_ansible.py @@ -126,8 +126,27 @@ def install(self, pkg_name: str): if self.run_user: cmd.append("--user") - self.do_as([*cmd, "--upgrade", "pip"]) + self.__upgrade_pip(cmd) + LOG.info("Installing the %s package", pkg_name) self.do_as([*cmd, pkg_name]) + LOG.info("Installed the %s package", pkg_name) + + def __upgrade_pip(self, cmd: list): + """Try to upgrade pip then not raise an exception + if the pip upgrade fails""" + LOG.info("Upgrading pip") + try: + self.do_as([*cmd, "--upgrade", "pip"]) + except subp.ProcessExecutionError as e: + LOG.warning( + ( + "Failed at upgrading pip. This is usually not critical" + "so the script will skip this step.\n%s" + ), + e, + ) + else: + LOG.info("Upgraded pip") def is_installed(self) -> bool: cmd = [sys.executable, "-m", "pip", "list"] diff --git a/tests/unittests/config/test_cc_ansible.py b/tests/unittests/config/test_cc_ansible.py index dba336e113c..3f3353b25d7 100644 --- a/tests/unittests/config/test_cc_ansible.py +++ b/tests/unittests/config/test_cc_ansible.py @@ -1,5 +1,6 @@ import os import re +from contextlib import nullcontext from copy import deepcopy from textwrap import dedent from typing import Any, Dict @@ -15,6 +16,7 @@ get_schema, validate_cloudconfig_schema, ) +from cloudinit.subp import ProcessExecutionError from tests.unittests.helpers import ( skipUnlessJsonSchema, skipUnlessJsonSchemaVersionGreaterThan, @@ -550,6 +552,65 @@ def test_pip_bootstrap(self, m_which, m_subp): cc_ansible.AnsiblePullPip(distro, "ansible").install("") distro.install_packages.assert_called_once() + @mark.parametrize( + "mocked_doas_kwargs,expected_result,expected_present_logs,expected_absent_logs", + [ + ( + {"return_value": ("", "")}, + nullcontext(), + ["Upgraded pip"], + ["Failed at upgrading pip"], + ), + # When do_as raises a ProcessExecutionError, __upgrade_pip will + # catch it then return without raising it + ( + {"side_effect": ProcessExecutionError()}, + nullcontext(), + ["Failed at upgrading pip"], + ["Upgraded pip"], + ), + # The below use case is to ensure that the function will raise the + # error if the error is not ProcessExecutionError (i.e. something + # went really sideways with do_as and not simply a non-zero exit + # by the executed command) + ( + {"side_effect": ZeroDivisionError()}, + raises(ZeroDivisionError), + [], + ["Failed at upgrading pip", "Upgraded pip"], + ), + ], + ) + def test_upgrade_pip( + self, + mocked_doas_kwargs, + expected_result, + expected_present_logs, + expected_absent_logs, + caplog, + ): + """assert __upgrade_pip will succeed + or will catch an exception and not fail""" + distro = get_cloud(mocked_distro=True).distro + pip = cc_ansible.AnsiblePullPip(distro, "ansible") + base_cmd = ["echo", "abc"] + with mock.patch.object( + pip, "do_as", **mocked_doas_kwargs + ) as mocked_doas: + with expected_result as e: + assert ( + getattr(pip, "_AnsiblePullPip__upgrade_pip")(base_cmd) == e + ) + + mocked_doas.assert_called_once() + mocked_doas.assert_called_with([*base_cmd, "--upgrade", "pip"]) + + for expected_present_log in expected_present_logs: + assert expected_present_log in caplog.text + + for expected_absent_log in expected_absent_logs: + assert expected_absent_log not in caplog.text + @mock.patch(M_PATH + "subp.which", return_value=True) @mock.patch(M_PATH + "subp.subp", return_value=(distro_version, "stderr")) @mock.patch( From 699b0afab1be325328be48f27c0529fc8e5dc4cf Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Tue, 22 Jul 2025 23:38:00 +0200 Subject: [PATCH 037/150] test: drop citestcase from azure source test (#6320) --- tests/unittests/sources/test_azure.py | 1399 ++++++++++++------------- 1 file changed, 696 insertions(+), 703 deletions(-) diff --git a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py index f59fcf2d611..fbc90a2d853 100644 --- a/tests/unittests/sources/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -4,10 +4,10 @@ import copy import datetime import json +import logging import os import stat import xml.etree.ElementTree as ET -from contextlib import ExitStack from pathlib import Path import passlib.hash @@ -28,7 +28,7 @@ write_file, ) from tests.unittests.helpers import ( - CiTestCase, + assert_count_equal, example_netdev, mock, populate_dir, @@ -1050,60 +1050,49 @@ def test_uses_fallback_cfg_when_no_interface_metadata( assert azure_ds.network_config == self.fallback_config -class TestAzureDataSource(CiTestCase): - with_logs = True +@pytest.fixture +def waagent_d(tmp_path): + return str(tmp_path / "var/lib/waagent") - def setUp(self): - super(TestAzureDataSource, self).setUp() - self.tmp = self.tmp_dir() +# @pytest.mark.usefixtures("fake_filesystem") +class TestAzureDataSource: + @pytest.fixture(autouse=True) + def fixture(self, mocker): # patch cloud_dir, so our 'seed_dir' is guaranteed empty - self.paths = helpers.Paths( - {"cloud_dir": self.tmp, "run_dir": self.tmp} - ) - self.waagent_d = os.path.join(self.tmp, "var", "lib", "waagent") - - self.patches = ExitStack() - self.addCleanup(self.patches.close) + mocker.patch.object(dsaz, "_get_random_seed", return_value="wild") - self.patches.enter_context( - mock.patch.object(dsaz, "_get_random_seed", return_value="wild") - ) - - self.m_dhcp = self.patches.enter_context( - mock.patch.object( - dsaz, - "EphemeralDHCPv4", - ) + self.m_dhcp = mocker.patch.object( + dsaz, + "EphemeralDHCPv4", ) self.m_dhcp.return_value.lease = {} self.m_dhcp.return_value.iface = "eth4" - self.m_fetch = self.patches.enter_context( - mock.patch.object( - dsaz.imds, - "fetch_metadata_with_api_fallback", - mock.MagicMock(return_value=NETWORK_METADATA), - ) + self.m_fetch = mocker.patch.object( + dsaz.imds, + "fetch_metadata_with_api_fallback", + mock.MagicMock(return_value=NETWORK_METADATA), ) - self.m_fallback_nic = self.patches.enter_context( - mock.patch( - "cloudinit.sources.net.find_fallback_nic", return_value="eth9" - ) + self.m_fallback_nic = mocker.patch( + "cloudinit.sources.net.find_fallback_nic", return_value="eth9" ) - self.m_remove_ubuntu_network_scripts = self.patches.enter_context( - mock.patch.object( - dsaz, - "maybe_remove_ubuntu_network_config_scripts", - mock.MagicMock(), - ) + self.m_remove_ubuntu_network_scripts = mocker.patch.object( + dsaz, + "maybe_remove_ubuntu_network_config_scripts", + mock.MagicMock(), ) - def apply_patches(self, patches): - for module, name, new in patches: - self.patches.enter_context(mock.patch.object(module, name, new)) + @pytest.fixture + def apply_patches(self, mocker): + def _apply_patches(patches): + for module, name, new in patches: + mocker.patch.object(module, name, new) + + return _apply_patches - def _get_mockds(self): + @pytest.fixture + def _get_mockds(self, apply_patches): sysctl_out = ( "dev.storvsc.3.%pnpinfo: " "classid=ba6163d9-04a1-4d29-b605-72e2ffb1dc7f " @@ -1133,7 +1122,7 @@ def _get_mockds(self): at scbus2 target 0 lun 0 (da0,pass1) at scbus3 target 1 lun 0 (da1,pass2) """ - self.apply_patches( + apply_patches( [ ( dsaz, @@ -1154,105 +1143,108 @@ def _get_mockds(self): ) return dsaz - def _get_ds( - self, - data, - distro="ubuntu", - apply_network=None, - instance_id=None, - write_ovf_to_data_dir: bool = False, - write_ovf_to_seed_dir: bool = True, - ): - def _wait_for_files(flist, _maxwait=None, _naplen=None): - data["waited"] = flist - return [] - - def _load_possible_azure_ds(seed_dir, cache_dir): - yield seed_dir - yield dsaz.DEFAULT_PROVISIONING_ISO_DEV - yield from data.get("dsdevs", []) - if cache_dir: - yield cache_dir - - seed_dir = os.path.join(self.paths.seed_dir, "azure") - if write_ovf_to_seed_dir and data.get("ovfcontent") is not None: - populate_dir(seed_dir, {"ovf-env.xml": data["ovfcontent"]}) - - if write_ovf_to_data_dir and data.get("ovfcontent") is not None: - populate_dir(self.waagent_d, {"ovf-env.xml": data["ovfcontent"]}) - - dsaz.BUILTIN_DS_CONFIG["data_dir"] = self.waagent_d - - self.m_get_metadata_from_fabric = mock.MagicMock(return_value=[]) - self.m_report_failure_to_fabric = mock.MagicMock(autospec=True) - self.m_list_possible_azure_ds = mock.MagicMock( - side_effect=_load_possible_azure_ds - ) + @pytest.fixture + def get_ds(self, apply_patches, paths, tmp_path, waagent_d): + def _get_ds( + data, + distro="ubuntu", + apply_network=None, + instance_id=None, + write_ovf_to_data_dir: bool = False, + write_ovf_to_seed_dir: bool = True, + ): + def _wait_for_files(flist, _maxwait=None, _naplen=None): + data["waited"] = flist + return [] + + def _load_possible_azure_ds(seed_dir, cache_dir): + yield seed_dir + yield dsaz.DEFAULT_PROVISIONING_ISO_DEV + yield from data.get("dsdevs", []) + if cache_dir: + yield cache_dir + + seed_dir = os.path.join(paths.seed_dir, "azure") + if write_ovf_to_seed_dir and data.get("ovfcontent") is not None: + populate_dir(seed_dir, {"ovf-env.xml": data["ovfcontent"]}) + + if write_ovf_to_data_dir and data.get("ovfcontent") is not None: + populate_dir(waagent_d, {"ovf-env.xml": data["ovfcontent"]}) + + dsaz.BUILTIN_DS_CONFIG["data_dir"] = waagent_d + + self.m_get_metadata_from_fabric = mock.MagicMock(return_value=[]) + self.m_report_failure_to_fabric = mock.MagicMock(autospec=True) + self.m_list_possible_azure_ds = mock.MagicMock( + side_effect=_load_possible_azure_ds + ) - if instance_id: - self.instance_id = instance_id - else: - self.instance_id = EXAMPLE_UUID + if instance_id: + self.instance_id = instance_id + else: + self.instance_id = EXAMPLE_UUID - def _dmi_mocks(key): - if key == "system-uuid": - return self.instance_id - elif key == "chassis-asset-tag": - return "7783-7084-3265-9085-8269-3286-77" - raise RuntimeError() + def _dmi_mocks(key): + if key == "system-uuid": + return self.instance_id + elif key == "chassis-asset-tag": + return "7783-7084-3265-9085-8269-3286-77" + raise RuntimeError() - self.m_read_dmi_data = mock.MagicMock(autospec=True) - self.m_read_dmi_data.side_effect = _dmi_mocks + self.m_read_dmi_data = mock.MagicMock(autospec=True) + self.m_read_dmi_data.side_effect = _dmi_mocks - self.apply_patches( - [ - ( - dsaz, - "list_possible_azure_ds", - self.m_list_possible_azure_ds, - ), - ( - dsaz, - "get_metadata_from_fabric", - self.m_get_metadata_from_fabric, - ), - ( - dsaz, - "report_failure_to_fabric", - self.m_report_failure_to_fabric, - ), - (dsaz, "get_boot_telemetry", mock.MagicMock()), - (dsaz, "get_system_info", mock.MagicMock()), - ( - dsaz.net, - "get_interface_mac", - mock.MagicMock(return_value="00:15:5d:69:63:ba"), - ), - (dsaz.subp, "which", lambda x: True), - ( - dmi, - "read_dmi_data", - self.m_read_dmi_data, - ), - ( - dsaz.util, - "wait_for_files", - mock.MagicMock(side_effect=_wait_for_files), - ), - ] - ) + apply_patches( + [ + ( + dsaz, + "list_possible_azure_ds", + self.m_list_possible_azure_ds, + ), + ( + dsaz, + "get_metadata_from_fabric", + self.m_get_metadata_from_fabric, + ), + ( + dsaz, + "report_failure_to_fabric", + self.m_report_failure_to_fabric, + ), + (dsaz, "get_boot_telemetry", mock.MagicMock()), + (dsaz, "get_system_info", mock.MagicMock()), + ( + dsaz.net, + "get_interface_mac", + mock.MagicMock(return_value="00:15:5d:69:63:ba"), + ), + (dsaz.subp, "which", lambda x: True), + ( + dmi, + "read_dmi_data", + self.m_read_dmi_data, + ), + ( + dsaz.util, + "wait_for_files", + mock.MagicMock(side_effect=_wait_for_files), + ), + ] + ) - if isinstance(distro, str): - distro_cls = distros.fetch(distro) - distro = distro_cls(distro, data.get("sys_cfg", {}), self.paths) - distro.get_tmp_exec_path = mock.Mock(side_effect=self.tmp_dir) - dsrc = dsaz.DataSourceAzure( - data.get("sys_cfg", {}), distro=distro, paths=self.paths - ) - if apply_network is not None: - dsrc.ds_cfg["apply_network_config"] = apply_network + if isinstance(distro, str): + distro_cls = distros.fetch(distro) + distro = distro_cls(distro, data.get("sys_cfg", {}), paths) + distro.get_tmp_exec_path = mock.Mock(side_effect=str(tmp_path)) + dsrc = dsaz.DataSourceAzure( + data.get("sys_cfg", {}), distro=distro, paths=paths + ) + if apply_network is not None: + dsrc.ds_cfg["apply_network_config"] = apply_network - return dsrc + return dsrc + + return _get_ds def _get_and_setup(self, dsrc): ret = dsrc.get_data() @@ -1293,16 +1285,16 @@ def xml_notequals(self, oxml, nxml): return raise AssertionError("XML is the same") - def test_get_resource_disk(self): - ds = self._get_mockds() + def test_get_resource_disk(self, _get_mockds): + ds = _get_mockds dev = ds.get_resource_disk_on_freebsd(1) - self.assertEqual("da1", dev) + assert "da1" == dev - def test_not_ds_detect_seed_should_return_no_datasource(self): + def test_not_ds_detect_seed_should_return_no_datasource(self, get_ds): """Check seed_dir using ds_detect and return False.""" # Return a non-matching asset tag value data = {} - dsrc = self._get_ds(data) + dsrc = get_ds(data) self.m_read_dmi_data.side_effect = lambda x: "notazure" with mock.patch.object( dsrc, "crawl_metadata" @@ -1313,38 +1305,40 @@ def test_not_ds_detect_seed_should_return_no_datasource(self): assert self.m_read_dmi_data.mock_calls == [ mock.call("chassis-asset-tag") ] - self.assertFalse(ret) + assert not ret # Assert that for non viable platforms, # there is no communication with the Azure datasource. - self.assertEqual(0, m_crawl_metadata.call_count) - self.assertEqual(0, m_report_failure.call_count) + assert 0 == m_crawl_metadata.call_count + assert 0 == m_report_failure.call_count - def test_platform_viable_but_no_devs_should_return_no_datasource(self): + def test_platform_viable_but_no_devs_should_return_no_datasource( + self, get_ds, mocker + ): """For platforms where the Azure platform is viable (which is indicated by the matching asset tag), the absence of any devs at all (devs == candidate sources for crawling Azure datasource) is NOT expected. - Report failure to Azure as this is an unexpected fatal error. + Report failure to Azure as this is an unexpected error. """ data = {} - dsrc = self._get_ds(data) + dsrc = get_ds(data) + mocker.patch(MOCKPATH + "list_possible_azure_ds", return_value=[]) with mock.patch.object(dsrc, "_report_failure") as m_report_failure: - ret = dsrc.get_data() - self.assertFalse(ret) - self.assertEqual(1, m_report_failure.call_count) + assert dsrc.get_data() is True + assert 1 == m_report_failure.call_count - def test_crawl_metadata_exception_returns_no_datasource(self): + def test_crawl_metadata_exception_returns_no_datasource(self, get_ds): data = {} - dsrc = self._get_ds(data) + dsrc = get_ds(data) with mock.patch.object(dsrc, "crawl_metadata") as m_crawl_metadata: m_crawl_metadata.side_effect = Exception ret = dsrc.get_data() - self.assertEqual(1, m_crawl_metadata.call_count) - self.assertFalse(ret) + assert 1 == m_crawl_metadata.call_count + assert not ret - def test_crawl_metadata_exception_should_report_failure(self): + def test_crawl_metadata_exception_should_report_failure(self, get_ds): data = {} - dsrc = self._get_ds(data) + dsrc = get_ds(data) with mock.patch.object( dsrc, "crawl_metadata" ) as m_crawl_metadata, mock.patch.object( @@ -1352,45 +1346,42 @@ def test_crawl_metadata_exception_should_report_failure(self): ) as m_report_failure: m_crawl_metadata.side_effect = Exception dsrc.get_data() - self.assertEqual(1, m_crawl_metadata.call_count) + assert 1 == m_crawl_metadata.call_count m_report_failure.assert_called_once_with(mock.ANY) - def test_crawl_metadata_exc_should_log_could_not_crawl_msg(self): + def test_crawl_metadata_exc_should_log_could_not_crawl_msg( + self, caplog, get_ds + ): data = {} - dsrc = self._get_ds(data) + dsrc = get_ds(data) with mock.patch.object(dsrc, "crawl_metadata") as m_crawl_metadata: m_crawl_metadata.side_effect = Exception dsrc.get_data() - self.assertEqual(1, m_crawl_metadata.call_count) - self.assertIn( - "Azure datasource failure occurred:", self.logs.getvalue() - ) + assert 1 == m_crawl_metadata.call_count + assert "Azure datasource failure occurred:" in caplog.text - def test_basic_seed_dir(self): + def test_basic_seed_dir(self, get_ds, paths, waagent_d): data = { "ovfcontent": construct_ovf_env(hostname="myhost"), "sys_cfg": {}, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, "") - self.assertEqual(dsrc.metadata["local-hostname"], "myhost") - self.assertTrue( - os.path.isfile(os.path.join(self.waagent_d, "ovf-env.xml")) - ) - self.assertEqual("azure", dsrc.cloud_name) - self.assertEqual("azure", dsrc.platform_type) - self.assertEqual( - "seed-dir (%s/seed/azure)" % self.tmp, dsrc.subplatform - ) - - def test_data_dir_without_imds_data(self): + assert ret + assert dsrc.userdata_raw == "" + assert dsrc.metadata["local-hostname"] == "myhost" + assert os.path.isfile(os.path.join(waagent_d, "ovf-env.xml")) + assert "azure" == dsrc.cloud_name + assert "azure" == dsrc.platform_type + seed_dir = os.path.join(paths.seed_dir, "azure") + assert "seed-dir (%s)" % seed_dir == dsrc.subplatform + + def test_data_dir_without_imds_data(self, get_ds, waagent_d): data = { "ovfcontent": construct_ovf_env(hostname="myhost"), "sys_cfg": {}, } - dsrc = self._get_ds( + dsrc = get_ds( data, write_ovf_to_data_dir=True, write_ovf_to_seed_dir=False ) @@ -1402,20 +1393,18 @@ def test_data_dir_without_imds_data(self): ] ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, "") - self.assertEqual(dsrc.metadata["local-hostname"], "myhost") - self.assertTrue( - os.path.isfile(os.path.join(self.waagent_d, "ovf-env.xml")) - ) - self.assertEqual("azure", dsrc.cloud_name) - self.assertEqual("azure", dsrc.platform_type) - self.assertEqual("seed-dir (%s)" % self.waagent_d, dsrc.subplatform) + assert ret + assert dsrc.userdata_raw == "" + assert dsrc.metadata["local-hostname"] == "myhost" + assert os.path.isfile(os.path.join(waagent_d, "ovf-env.xml")) + assert "azure" == dsrc.cloud_name + assert "azure" == dsrc.platform_type + assert "seed-dir (%s)" % waagent_d == dsrc.subplatform - def test_basic_dev_file(self): + def test_basic_dev_file(self, get_ds): """When a device path is used, present that in subplatform.""" data = {"sys_cfg": {}, "dsdevs": ["/dev/cd0"]} - dsrc = self._get_ds(data) + dsrc = get_ds(data) # DSAzure will attempt to mount /dev/sr0 first, which should # fail with mount error since the list of devices doesn't have # /dev/sr0 @@ -1424,14 +1413,14 @@ def test_basic_dev_file(self): MountFailedError("fail"), ({"local-hostname": "me"}, "ud", {"cfg": ""}, {}), ] - self.assertTrue(dsrc.get_data()) - self.assertEqual(dsrc.userdata_raw, "ud") - self.assertEqual(dsrc.metadata["local-hostname"], "me") - self.assertEqual("azure", dsrc.cloud_name) - self.assertEqual("azure", dsrc.platform_type) - self.assertEqual("config-disk (/dev/cd0)", dsrc.subplatform) - - def test_get_data_non_ubuntu_will_not_remove_network_scripts(self): + assert dsrc.get_data() + assert dsrc.userdata_raw == "ud" + assert dsrc.metadata["local-hostname"] == "me" + assert "azure" == dsrc.cloud_name + assert "azure" == dsrc.platform_type + assert "config-disk (/dev/cd0)" == dsrc.subplatform + + def test_get_data_non_ubuntu_will_not_remove_network_scripts(self, get_ds): """get_data on non-Ubuntu will not remove ubuntu net scripts.""" data = { "ovfcontent": construct_ovf_env( @@ -1440,11 +1429,11 @@ def test_get_data_non_ubuntu_will_not_remove_network_scripts(self): "sys_cfg": {}, } - dsrc = self._get_ds(data, distro="debian") + dsrc = get_ds(data, distro="debian") dsrc.get_data() self.m_remove_ubuntu_network_scripts.assert_not_called() - def test_get_data_on_ubuntu_will_remove_network_scripts(self): + def test_get_data_on_ubuntu_will_remove_network_scripts(self, get_ds): """get_data will remove ubuntu net scripts on Ubuntu distro.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { @@ -1452,11 +1441,13 @@ def test_get_data_on_ubuntu_will_remove_network_scripts(self): "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data, distro="ubuntu") + dsrc = get_ds(data, distro="ubuntu") dsrc.get_data() self.m_remove_ubuntu_network_scripts.assert_called_once_with() - def test_get_data_on_ubuntu_will_not_remove_network_scripts_disabled(self): + def test_get_data_on_ubuntu_will_not_remove_network_scripts_disabled( + self, get_ds + ): """When apply_network_config false, do not remove scripts on Ubuntu.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": False}}} data = { @@ -1464,11 +1455,13 @@ def test_get_data_on_ubuntu_will_not_remove_network_scripts_disabled(self): "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data, distro="ubuntu") + dsrc = get_ds(data, distro="ubuntu") dsrc.get_data() self.m_remove_ubuntu_network_scripts.assert_not_called() - def test_crawl_metadata_returns_structured_data_and_caches_nothing(self): + def test_crawl_metadata_returns_structured_data_and_caches_nothing( + self, get_ds, waagent_d + ): """Return all structured metadata and cache no class attributes.""" data = { "ovfcontent": construct_ovf_env( @@ -1476,7 +1469,7 @@ def test_crawl_metadata_returns_structured_data_and_caches_nothing(self): ), "sys_cfg": {}, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) expected_cfg = { "PreprovisionedVMType": None, "PreprovisionedVm": False, @@ -1492,46 +1485,40 @@ def test_crawl_metadata_returns_structured_data_and_caches_nothing(self): crawled_metadata = dsrc.crawl_metadata() - self.assertCountEqual( + assert_count_equal( crawled_metadata.keys(), ["cfg", "files", "metadata", "userdata_raw"], ) - self.assertEqual(crawled_metadata["cfg"], expected_cfg) - self.assertEqual( - list(crawled_metadata["files"].keys()), ["ovf-env.xml"] - ) - self.assertIn( - b"myhost", - crawled_metadata["files"]["ovf-env.xml"], - ) - self.assertEqual(crawled_metadata["metadata"], expected_metadata) - self.assertEqual(crawled_metadata["userdata_raw"], b"FOOBAR") - self.assertEqual(dsrc.userdata_raw, None) - self.assertEqual(dsrc.metadata, {}) - self.assertEqual(dsrc._metadata_imds, UNSET) - self.assertFalse( - os.path.isfile(os.path.join(self.waagent_d, "ovf-env.xml")) + assert crawled_metadata["cfg"] == expected_cfg + assert list(crawled_metadata["files"].keys()) == ["ovf-env.xml"] + assert ( + b"myhost" + in crawled_metadata["files"]["ovf-env.xml"] ) - - def test_crawl_metadata_raises_invalid_metadata_on_error(self): + assert crawled_metadata["metadata"] == expected_metadata + assert crawled_metadata["userdata_raw"] == b"FOOBAR" + assert dsrc.userdata_raw is None + assert dsrc.metadata == {} + assert dsrc._metadata_imds == UNSET + assert not os.path.isfile(os.path.join(waagent_d, "ovf-env.xml")) + + def test_crawl_metadata_raises_invalid_metadata_on_error(self, get_ds): """crawl_metadata raises an exception on invalid ovf-env.xml.""" data = {"ovfcontent": "BOGUS", "sys_cfg": {}} - dsrc = self._get_ds(data) + dsrc = get_ds(data) error_msg = "error parsing ovf-env.xml: syntax error: line 1, column 0" - with self.assertRaises( - errors.ReportableErrorOvfParsingException - ) as cm: + with pytest.raises(errors.ReportableErrorOvfParsingException) as cm: dsrc.crawl_metadata() - self.assertEqual(cm.exception.reason, error_msg) + assert cm.value.reason == error_msg - def test_crawl_metadata_call_imds_once_no_reprovision(self): + def test_crawl_metadata_call_imds_once_no_reprovision(self, get_ds): """If reprovisioning, report ready at the end""" ovfenv = construct_ovf_env(preprovisioned_vm=False) data = {"ovfcontent": ovfenv, "sys_cfg": {}} - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.crawl_metadata() - self.assertEqual(1, self.m_fetch.call_count) + assert 1 == self.m_fetch.call_count @mock.patch("cloudinit.sources.DataSourceAzure.util.write_file") @mock.patch( @@ -1539,26 +1526,26 @@ def test_crawl_metadata_call_imds_once_no_reprovision(self): ) @mock.patch("cloudinit.sources.DataSourceAzure.DataSourceAzure._poll_imds") def test_crawl_metadata_call_imds_twice_with_reprovision( - self, poll_imds_func, m_report_ready, m_write + self, poll_imds_func, m_report_ready, m_write, get_ds ): """If reprovisioning, imds metadata will be fetched twice""" ovfenv = construct_ovf_env(preprovisioned_vm=True) data = {"ovfcontent": ovfenv, "sys_cfg": {}} - dsrc = self._get_ds(data) + dsrc = get_ds(data) poll_imds_func.return_value = ovfenv dsrc.crawl_metadata() - self.assertEqual(2, self.m_fetch.call_count) + assert 2 == self.m_fetch.call_count - def test_waagent_d_has_0700_perms(self): + def test_waagent_d_has_0700_perms(self, get_ds, waagent_d): # we expect /var/lib/waagent to be created 0700 - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertTrue(os.path.isdir(self.waagent_d)) - self.assertEqual(stat.S_IMODE(os.stat(self.waagent_d).st_mode), 0o700) + assert ret + assert os.path.isdir(waagent_d) + assert stat.S_IMODE(os.stat(waagent_d).st_mode) == 0o700 - def test_network_config_set_from_imds(self): + def test_network_config_set_from_imds(self, get_ds): """Datasource.network_config returns IMDS network data.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { @@ -1577,11 +1564,13 @@ def test_network_config_set_from_imds(self): }, "version": 2, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual(expected_network_config, dsrc.network_config) + assert expected_network_config == dsrc.network_config - def test_network_config_set_from_imds_route_metric_for_secondary_nic(self): + def test_network_config_set_from_imds_route_metric_for_secondary_nic( + self, get_ds + ): """Datasource.network_config adds route-metric to secondary nics.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { @@ -1623,11 +1612,13 @@ def test_network_config_set_from_imds_route_metric_for_secondary_nic(self): imds_data["network"]["interface"].append(third_intf) self.m_fetch.return_value = imds_data - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual(expected_network_config, dsrc.network_config) + assert expected_network_config == dsrc.network_config - def test_network_config_set_from_imds_for_secondary_nic_no_ip(self): + def test_network_config_set_from_imds_for_secondary_nic_no_ip( + self, get_ds + ): """If an IP address is empty then there should no config for it.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { @@ -1649,33 +1640,33 @@ def test_network_config_set_from_imds_for_secondary_nic_no_ip(self): imds_data = copy.deepcopy(NETWORK_METADATA) imds_data["network"]["interface"].append(SECONDARY_INTERFACE_NO_IP) self.m_fetch.return_value = imds_data - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual(expected_network_config, dsrc.network_config) + assert expected_network_config == dsrc.network_config - def test_availability_zone_set_from_imds(self): + def test_availability_zone_set_from_imds(self, get_ds): """Datasource.availability returns IMDS platformFaultDomain.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual("0", dsrc.availability_zone) + assert "0" == dsrc.availability_zone - def test_region_set_from_imds(self): + def test_region_set_from_imds(self, get_ds): """Datasource.region returns IMDS region location.""" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual("eastus2", dsrc.region) + assert "eastus2" == dsrc.region - def test_sys_cfg_set_never_destroy_ntfs(self): + def test_sys_cfg_set_never_destroy_ntfs(self, get_ds): sys_cfg = { "datasource": { "Azure": {"never_destroy_ntfs": "user-supplied-value"} @@ -1686,20 +1677,20 @@ def test_sys_cfg_set_never_destroy_ntfs(self): "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = self._get_and_setup(dsrc) - self.assertTrue(ret) - self.assertEqual( - dsrc.ds_cfg.get(dsaz.DS_CFG_KEY_PRESERVE_NTFS), - "user-supplied-value", + assert ret + assert ( + dsrc.ds_cfg.get(dsaz.DS_CFG_KEY_PRESERVE_NTFS) + == "user-supplied-value" ) - def test_no_admin_username(self): + def test_no_admin_username(self, get_ds): data = {"ovfcontent": construct_ovf_env(username=None)} - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) + assert ret assert dsrc.cfg == { "PreprovisionedVMType": None, @@ -1707,46 +1698,42 @@ def test_no_admin_username(self): "ProvisionGuestProxyAgent": False, } - def test_username_used(self): + def test_username_used(self, get_ds): data = {"ovfcontent": construct_ovf_env(username="myuser")} - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual( - dsrc.cfg["system_info"]["default_user"]["name"], "myuser" - ) + assert ret + assert dsrc.cfg["system_info"]["default_user"]["name"] == "myuser" assert "ssh_pwauth" not in dsrc.cfg - def test_password_given(self): + def test_password_given(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", password="mypass" ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertIn("default_user", dsrc.cfg["system_info"]) + assert ret + assert "default_user" in dsrc.cfg["system_info"] defuser = dsrc.cfg["system_info"]["default_user"] # default user should be updated username and should not be locked. - self.assertEqual(defuser["name"], "myuser") - self.assertFalse(defuser["lock_passwd"]) + assert defuser["name"] == "myuser" + assert not defuser["lock_passwd"] # passwd is crypt formated string $id$salt$encrypted # encrypting plaintext with salt value of everything up to final '$' # should equal that after the '$' - self.assertTrue( - passlib.hash.sha512_crypt.verify( - "mypass", defuser["hashed_passwd"] - ) + assert passlib.hash.sha512_crypt.verify( + "mypass", defuser["hashed_passwd"] ) assert dsrc.cfg["ssh_pwauth"] is True - def test_password_with_disable_ssh_pw_auth_true(self): + def test_password_with_disable_ssh_pw_auth_true(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1755,12 +1742,12 @@ def test_password_with_disable_ssh_pw_auth_true(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() assert dsrc.cfg["ssh_pwauth"] is False - def test_password_with_disable_ssh_pw_auth_false(self): + def test_password_with_disable_ssh_pw_auth_false(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1769,12 +1756,12 @@ def test_password_with_disable_ssh_pw_auth_false(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() assert dsrc.cfg["ssh_pwauth"] is True - def test_password_with_disable_ssh_pw_auth_unspecified(self): + def test_password_with_disable_ssh_pw_auth_unspecified(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1783,12 +1770,12 @@ def test_password_with_disable_ssh_pw_auth_unspecified(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() assert dsrc.cfg["ssh_pwauth"] is True - def test_no_password_with_disable_ssh_pw_auth_true(self): + def test_no_password_with_disable_ssh_pw_auth_true(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1796,12 +1783,12 @@ def test_no_password_with_disable_ssh_pw_auth_true(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() assert dsrc.cfg["ssh_pwauth"] is False - def test_no_password_with_disable_ssh_pw_auth_false(self): + def test_no_password_with_disable_ssh_pw_auth_false(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1809,12 +1796,12 @@ def test_no_password_with_disable_ssh_pw_auth_false(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() assert dsrc.cfg["ssh_pwauth"] is True - def test_no_password_with_disable_ssh_pw_auth_unspecified(self): + def test_no_password_with_disable_ssh_pw_auth_unspecified(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1822,12 +1809,12 @@ def test_no_password_with_disable_ssh_pw_auth_unspecified(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() assert "ssh_pwauth" not in dsrc.cfg - def test_user_not_locked_if_password_redacted(self): + def test_user_not_locked_if_password_redacted(self, get_ds): data = { "ovfcontent": construct_ovf_env( username="myuser", @@ -1835,27 +1822,27 @@ def test_user_not_locked_if_password_redacted(self): ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertIn("default_user", dsrc.cfg["system_info"]) + assert ret + assert "default_user" in dsrc.cfg["system_info"] defuser = dsrc.cfg["system_info"]["default_user"] # default user should be updated username and should not be locked. - self.assertEqual(defuser["name"], "myuser") - self.assertIn("lock_passwd", defuser) - self.assertFalse(defuser["lock_passwd"]) + assert defuser["name"] == "myuser" + assert "lock_passwd" in defuser + assert not defuser["lock_passwd"] - def test_userdata_found(self): + def test_userdata_found(self, get_ds): mydata = "FOOBAR" data = {"ovfcontent": construct_ovf_env(custom_data=mydata)} - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, mydata.encode("utf-8")) + assert ret + assert dsrc.userdata_raw == mydata.encode("utf-8") - def test_default_ephemeral_configs_ephemeral_exists(self): + def test_default_ephemeral_configs_ephemeral_exists(self, get_ds): # make sure the ephemeral configs are correct if disk present data = { "ovfcontent": construct_ovf_env(), @@ -1870,21 +1857,21 @@ def changed_exists(path): ) with mock.patch(MOCKPATH + "os.path.exists", new=changed_exists): - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) + assert ret cfg = dsrc.get_config_obj() - self.assertEqual( - dsrc.device_name_to_device("ephemeral0"), - dsaz.RESOURCE_DISK_PATH, + assert ( + dsrc.device_name_to_device("ephemeral0") + == dsaz.RESOURCE_DISK_PATH ) assert "disk_setup" in cfg assert "fs_setup" in cfg - self.assertIsInstance(cfg["disk_setup"], dict) - self.assertIsInstance(cfg["fs_setup"], list) + assert isinstance(cfg["disk_setup"], dict) + assert isinstance(cfg["fs_setup"], list) - def test_default_ephemeral_configs_ephemeral_does_not_exist(self): + def test_default_ephemeral_configs_ephemeral_does_not_exist(self, get_ds): # make sure the ephemeral configs are correct if disk not present data = { "ovfcontent": construct_ovf_env(), @@ -1899,74 +1886,74 @@ def changed_exists(path): ) with mock.patch(MOCKPATH + "os.path.exists", new=changed_exists): - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) + assert ret cfg = dsrc.get_config_obj() assert "disk_setup" not in cfg assert "fs_setup" not in cfg - def test_userdata_arrives(self): + def test_userdata_arrives(self, get_ds): userdata = "This is my user-data" xml = construct_ovf_env(custom_data=userdata) data = {"ovfcontent": xml} - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual(userdata.encode("us-ascii"), dsrc.userdata_raw) + assert userdata.encode("us-ascii") == dsrc.userdata_raw - def test_password_redacted_in_ovf(self): + def test_password_redacted_in_ovf(self, get_ds, waagent_d): data = { "ovfcontent": construct_ovf_env( username="myuser", password="mypass" ) } - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - ovf_env_path = os.path.join(self.waagent_d, "ovf-env.xml") + assert ret + ovf_env_path = os.path.join(waagent_d, "ovf-env.xml") # The XML should not be same since the user password is redacted on_disk_ovf = load_text_file(ovf_env_path) self.xml_notequals(data["ovfcontent"], on_disk_ovf) # Make sure that the redacted password on disk is not used by CI - self.assertNotEqual( - dsrc.cfg.get("password"), dsaz.DEF_PASSWD_REDACTION - ) + assert dsrc.cfg.get("password") != dsaz.DEF_PASSWD_REDACTION # Make sure that the password was really encrypted et = ET.fromstring(on_disk_ovf) for elem in et.iter(): if "UserPassword" in elem.tag: - self.assertEqual(dsaz.DEF_PASSWD_REDACTION, elem.text) + assert dsaz.DEF_PASSWD_REDACTION == elem.text - def test_ovf_env_arrives_in_waagent_dir(self): + def test_ovf_env_arrives_in_waagent_dir(self, get_ds, waagent_d): xml = construct_ovf_env(custom_data="FOODATA") - dsrc = self._get_ds({"ovfcontent": xml}) + dsrc = get_ds({"ovfcontent": xml}) dsrc.get_data() # 'data_dir' is '/var/lib/waagent' (walinux-agent's state dir) # we expect that the ovf-env.xml file is copied there. - ovf_env_path = os.path.join(self.waagent_d, "ovf-env.xml") - self.assertTrue(os.path.exists(ovf_env_path)) + ovf_env_path = os.path.join(waagent_d, "ovf-env.xml") + assert os.path.exists(ovf_env_path) self.xml_equals(xml, load_text_file(ovf_env_path)) - def test_ovf_can_include_unicode(self): + def test_ovf_can_include_unicode(self, get_ds): xml = construct_ovf_env() xml = "\ufeff{0}".format(xml) - dsrc = self._get_ds({"ovfcontent": xml}) + dsrc = get_ds({"ovfcontent": xml}) dsrc.get_data() - def test_dsaz_report_ready_returns_true_when_report_succeeds(self): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_dsaz_report_ready_returns_true_when_report_succeeds(self, get_ds): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) assert dsrc._report_ready() == [] @mock.patch(MOCKPATH + "report_diagnostic_event") - def test_dsaz_report_ready_failure_reports_telemetry(self, m_report_diag): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_dsaz_report_ready_failure_reports_telemetry( + self, m_report_diag, get_ds + ): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) self.m_get_metadata_from_fabric.side_effect = Exception("foo") with pytest.raises(Exception): @@ -1980,21 +1967,23 @@ def test_dsaz_report_ready_failure_reports_telemetry(self, m_report_diag): ) ] - def test_dsaz_report_failure_returns_true_when_report_succeeds(self): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_dsaz_report_failure_returns_true_when_report_succeeds( + self, get_ds + ): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) with mock.patch.object(dsrc, "crawl_metadata") as m_crawl_metadata: # mock crawl metadata failure to cause report failure m_crawl_metadata.side_effect = Exception error = errors.ReportableError(reason="foo") - self.assertTrue(dsrc._report_failure(error)) - self.assertEqual(1, self.m_report_failure_to_fabric.call_count) + assert dsrc._report_failure(error) + assert 1 == self.m_report_failure_to_fabric.call_count def test_dsaz_report_failure_returns_false_and_does_not_propagate_exc( - self, + self, get_ds ): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) with mock.patch.object( dsrc, "crawl_metadata" @@ -2020,24 +2009,26 @@ def test_dsaz_report_failure_returns_false_and_does_not_propagate_exc( self.m_report_failure_to_fabric.side_effect = Exception error = errors.ReportableError(reason="foo") - self.assertFalse(dsrc._report_failure(error)) - self.assertEqual(2, self.m_report_failure_to_fabric.call_count) + assert not dsrc._report_failure(error) + assert 2 == self.m_report_failure_to_fabric.call_count - def test_dsaz_report_failure(self): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_dsaz_report_failure(self, get_ds): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) with mock.patch.object(dsrc, "crawl_metadata") as m_crawl_metadata: m_crawl_metadata.side_effect = Exception error = errors.ReportableError(reason="foo") - self.assertTrue(dsrc._report_failure(error)) + assert dsrc._report_failure(error) self.m_report_failure_to_fabric.assert_called_once_with( endpoint="168.63.129.16", encoded_report=error.as_encoded_report(vm_id=dsrc._vm_id), ) - def test_dsaz_report_failure_uses_cached_ephemeral_dhcp_ctx_lease(self): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_dsaz_report_failure_uses_cached_ephemeral_dhcp_ctx_lease( + self, get_ds + ): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) with mock.patch.object( dsrc, "crawl_metadata" @@ -2048,7 +2039,7 @@ def test_dsaz_report_failure_uses_cached_ephemeral_dhcp_ctx_lease(self): m_crawl_metadata.side_effect = Exception error = errors.ReportableError(reason="foo") - self.assertTrue(dsrc._report_failure(error)) + assert dsrc._report_failure(error) # ensure called with cached ephemeral dhcp lease option 245 self.m_report_failure_to_fabric.assert_called_once_with( @@ -2056,8 +2047,10 @@ def test_dsaz_report_failure_uses_cached_ephemeral_dhcp_ctx_lease(self): encoded_report=error.as_encoded_report(vm_id=dsrc._vm_id), ) - def test_dsaz_report_failure_no_net_uses_new_ephemeral_dhcp_lease(self): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_dsaz_report_failure_no_net_uses_new_ephemeral_dhcp_lease( + self, get_ds + ): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) with mock.patch.object(dsrc, "crawl_metadata") as m_crawl_metadata: # mock crawl metadata failure to cause report failure @@ -2071,7 +2064,7 @@ def test_dsaz_report_failure_no_net_uses_new_ephemeral_dhcp_lease(self): self.m_dhcp.return_value.obtain_lease.return_value = test_lease error = errors.ReportableError(reason="foo") - self.assertTrue(dsrc._report_failure(error)) + assert dsrc._report_failure(error) # ensure called with the newly discovered # ephemeral dhcp lease option 245 @@ -2080,77 +2073,74 @@ def test_dsaz_report_failure_no_net_uses_new_ephemeral_dhcp_lease(self): encoded_report=error.as_encoded_report(vm_id=dsrc._vm_id), ) - def test_exception_fetching_fabric_data_doesnt_propagate(self): + def test_exception_fetching_fabric_data_doesnt_propagate(self, get_ds): """Errors communicating with fabric should warn, but return True.""" - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) self.m_get_metadata_from_fabric.side_effect = Exception ret = self._get_and_setup(dsrc) - self.assertTrue(ret) + assert ret - def test_fabric_data_included_in_metadata(self): - dsrc = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_fabric_data_included_in_metadata(self, get_ds): + dsrc = get_ds({"ovfcontent": construct_ovf_env()}) self.m_get_metadata_from_fabric.return_value = ["ssh-key-value"] ret = self._get_and_setup(dsrc) - self.assertTrue(ret) - self.assertEqual(["ssh-key-value"], dsrc.metadata["public-keys"]) + assert ret + assert ["ssh-key-value"] == dsrc.metadata["public-keys"] - def test_instance_id_case_insensitive(self): + def test_instance_id_case_insensitive(self, get_ds, paths): """Return the previous iid when current is a case-insensitive match.""" lower_iid = EXAMPLE_UUID.lower() upper_iid = EXAMPLE_UUID.upper() # lowercase current UUID - ds = self._get_ds( - {"ovfcontent": construct_ovf_env()}, instance_id=lower_iid - ) + ds = get_ds({"ovfcontent": construct_ovf_env()}, instance_id=lower_iid) # UPPERCASE previous write_file( - os.path.join(self.paths.cloud_dir, "data", "instance-id"), + os.path.join(paths.cloud_dir, "data", "instance-id"), upper_iid, ) ds.get_data() - self.assertEqual(upper_iid, ds.metadata["instance-id"]) + assert upper_iid == ds.metadata["instance-id"] # UPPERCASE current UUID - ds = self._get_ds( - {"ovfcontent": construct_ovf_env()}, instance_id=upper_iid - ) + ds = get_ds({"ovfcontent": construct_ovf_env()}, instance_id=upper_iid) # lowercase previous write_file( - os.path.join(self.paths.cloud_dir, "data", "instance-id"), + os.path.join(paths.cloud_dir, "data", "instance-id"), lower_iid, ) ds.get_data() - self.assertEqual(lower_iid, ds.metadata["instance-id"]) + assert lower_iid == ds.metadata["instance-id"] - def test_instance_id_endianness(self): + def test_instance_id_endianness(self, get_ds, paths): """Return the previous iid when dmi uuid is the byteswapped iid.""" - ds = self._get_ds({"ovfcontent": construct_ovf_env()}) + ds = get_ds({"ovfcontent": construct_ovf_env()}) # byte-swapped previous write_file( - os.path.join(self.paths.cloud_dir, "data", "instance-id"), + os.path.join(paths.cloud_dir, "data", "instance-id"), "544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8", ) ds.get_data() - self.assertEqual( - "544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8", ds.metadata["instance-id"] + assert ( + "544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8" + == ds.metadata["instance-id"] ) # not byte-swapped previous write_file( - os.path.join(self.paths.cloud_dir, "data", "instance-id"), + os.path.join(paths.cloud_dir, "data", "instance-id"), "644CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8", ) ds.get_data() - self.assertEqual(self.instance_id, ds.metadata["instance-id"]) + assert self.instance_id == ds.metadata["instance-id"] - def test_instance_id_from_dmidecode_used(self): - ds = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_instance_id_from_dmidecode_used(self, get_ds): + ds = get_ds({"ovfcontent": construct_ovf_env()}) ds.get_data() - self.assertEqual(self.instance_id, ds.metadata["instance-id"]) + assert self.instance_id == ds.metadata["instance-id"] - def test_instance_id_from_dmidecode_used_for_builtin(self): - ds = self._get_ds({"ovfcontent": construct_ovf_env()}) + def test_instance_id_from_dmidecode_used_for_builtin(self, get_ds): + ds = get_ds({"ovfcontent": construct_ovf_env()}) ds.get_data() - self.assertEqual(self.instance_id, ds.metadata["instance-id"]) + assert self.instance_id == ds.metadata["instance-id"] @mock.patch(MOCKPATH + "util.is_FreeBSD") @mock.patch(MOCKPATH + "_check_freebsd_cdrom") @@ -2161,21 +2151,18 @@ def test_list_possible_azure_ds(self, m_check_fbsd_cdrom, m_is_FreeBSD): possible_ds = [] for src in dsaz.list_possible_azure_ds("seed_dir", "cache_dir"): possible_ds.append(src) - self.assertEqual( - possible_ds, - [ - "seed_dir", - dsaz.DEFAULT_PROVISIONING_ISO_DEV, - "/dev/cd0", - "cache_dir", - ], - ) - self.assertEqual( - [mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list - ) + assert possible_ds == [ + "seed_dir", + dsaz.DEFAULT_PROVISIONING_ISO_DEV, + "/dev/cd0", + "cache_dir", + ] + assert [mock.call("/dev/cd0")] == m_check_fbsd_cdrom.call_args_list @mock.patch(MOCKPATH + "net.get_interfaces") - def test_blacklist_through_distro(self, m_net_get_interfaces): + def test_blacklist_through_distro( + self, m_net_get_interfaces, get_ds, paths + ): """Verify Azure DS updates blacklist drivers in the distro's networking object.""" data = { @@ -2184,8 +2171,8 @@ def test_blacklist_through_distro(self, m_net_get_interfaces): } distro_cls = distros.fetch("ubuntu") - distro = distro_cls("ubuntu", {}, self.paths) - dsrc = self._get_ds(data, distro=distro) + distro = distro_cls("ubuntu", {}, paths) + dsrc = get_ds(data, distro=distro) dsrc.get_data() distro.networking.get_interfaces_by_mac() @@ -2194,18 +2181,18 @@ def test_blacklist_through_distro(self, m_net_get_interfaces): @mock.patch( "cloudinit.sources.helpers.azure.OpenSSLManager.parse_certificates" ) - def test_get_public_ssh_keys_with_imds(self, m_parse_certificates): + def test_get_public_ssh_keys_with_imds(self, m_parse_certificates, get_ds): sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() dsrc.setup(True) ssh_keys = dsrc.get_public_ssh_keys() - self.assertEqual(ssh_keys, ["ssh-rsa key1"]) - self.assertEqual(m_parse_certificates.call_count, 0) + assert ssh_keys == ["ssh-rsa key1"] + assert m_parse_certificates.call_count == 0 def test_key_without_crlf_valid(self): test_key = "ssh-rsa somerandomkeystuff some comment" @@ -2223,7 +2210,7 @@ def test_key_endswith_crlf_valid(self): "cloudinit.sources.helpers.azure.OpenSSLManager.parse_certificates" ) def test_get_public_ssh_keys_with_no_openssh_format( - self, m_parse_certificates + self, m_parse_certificates, get_ds ): imds_data = copy.deepcopy(NETWORK_METADATA) imds_data["compute"]["publicKeys"][0]["keyData"] = "no-openssh-format" @@ -2233,28 +2220,28 @@ def test_get_public_ssh_keys_with_no_openssh_format( "ovfcontent": construct_ovf_env(), "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() dsrc.setup(True) ssh_keys = dsrc.get_public_ssh_keys() - self.assertEqual(ssh_keys, []) - self.assertEqual(m_parse_certificates.call_count, 0) + assert ssh_keys == [] + assert m_parse_certificates.call_count == 0 - def test_get_public_ssh_keys_without_imds(self): + def test_get_public_ssh_keys_without_imds(self, get_ds): self.m_fetch.return_value = dict() sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), "sys_cfg": sys_cfg, } - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsaz.get_metadata_from_fabric.return_value = ["key2"] dsrc.get_data() dsrc.setup(True) ssh_keys = dsrc.get_public_ssh_keys() - self.assertEqual(ssh_keys, ["key2"]) + assert ssh_keys == ["key2"] - def test_hostname_from_imds(self): + def test_hostname_from_imds(self, get_ds): sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), @@ -2267,11 +2254,11 @@ def test_hostname_from_imds(self): disablePasswordAuthentication="true", ) self.m_fetch.return_value = imds_data_with_os_profile - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual(dsrc.metadata["local-hostname"], "hostname1") + assert dsrc.metadata["local-hostname"] == "hostname1" - def test_username_from_imds(self): + def test_username_from_imds(self, get_ds): sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), @@ -2284,13 +2271,11 @@ def test_username_from_imds(self): disablePasswordAuthentication="true", ) self.m_fetch.return_value = imds_data_with_os_profile - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertEqual( - dsrc.cfg["system_info"]["default_user"]["name"], "username1" - ) + assert dsrc.cfg["system_info"]["default_user"]["name"] == "username1" - def test_disable_password_from_imds_true(self): + def test_disable_password_from_imds_true(self, get_ds): sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), @@ -2303,11 +2288,11 @@ def test_disable_password_from_imds_true(self): disablePasswordAuthentication="true", ) self.m_fetch.return_value = imds_data_with_os_profile - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertFalse(dsrc.cfg["ssh_pwauth"]) + assert not dsrc.cfg["ssh_pwauth"] - def test_disable_password_from_imds_false(self): + def test_disable_password_from_imds_false(self, get_ds): sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), @@ -2321,11 +2306,11 @@ def test_disable_password_from_imds_false(self): disablePasswordAuthentication="false", ) self.m_fetch.return_value = imds_data_with_os_profile - dsrc = self._get_ds(data) + dsrc = get_ds(data) dsrc.get_data() - self.assertTrue(dsrc.cfg["ssh_pwauth"]) + assert dsrc.cfg["ssh_pwauth"] - def test_userdata_from_imds(self): + def test_userdata_from_imds(self, get_ds): sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { "ovfcontent": construct_ovf_env(), @@ -2340,12 +2325,12 @@ def test_userdata_from_imds(self): ) imds_data["compute"]["userData"] = b64e(userdata) self.m_fetch.return_value = imds_data - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, userdata.encode("utf-8")) + assert ret + assert dsrc.userdata_raw == userdata.encode("utf-8") - def test_userdata_from_imds_with_customdata_from_OVF(self): + def test_userdata_from_imds_with_customdata_from_OVF(self, get_ds): userdataOVF = "userdataOVF" sys_cfg = {"datasource": {"Azure": {"apply_network_config": True}}} data = { @@ -2362,116 +2347,130 @@ def test_userdata_from_imds_with_customdata_from_OVF(self): ) imds_data["compute"]["userData"] = b64e(userdataImds) self.m_fetch.return_value = imds_data - dsrc = self._get_ds(data) + dsrc = get_ds(data) ret = dsrc.get_data() - self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, userdataOVF.encode("utf-8")) + assert ret + assert dsrc.userdata_raw == userdataOVF.encode("utf-8") -class TestLoadAzureDsDir(CiTestCase): +class TestLoadAzureDsDir: """Tests for load_azure_ds_dir.""" - def setUp(self): - self.source_dir = self.tmp_dir() - super(TestLoadAzureDsDir, self).setUp() - - def test_missing_ovf_env_xml_raises_non_azure_datasource_error(self): + def test_missing_ovf_env_xml_raises_non_azure_datasource_error( + self, tmp_path + ): """load_azure_ds_dir raises an error When ovf-env.xml doesn't exit.""" - with self.assertRaises(dsaz.NonAzureDataSource) as context_manager: - dsaz.load_azure_ds_dir(self.source_dir) - self.assertEqual( - "No ovf-env file found", str(context_manager.exception) - ) + with pytest.raises( + dsaz.NonAzureDataSource, match="No ovf-env file found" + ): + dsaz.load_azure_ds_dir(str(tmp_path)) - def test_wb_invalid_ovf_env_xml_calls_read_azure_ovf(self): + def test_wb_invalid_ovf_env_xml_calls_read_azure_ovf(self, tmp_path): """load_azure_ds_dir calls read_azure_ovf to parse the xml.""" - ovf_path = os.path.join(self.source_dir, "ovf-env.xml") + ovf_path = os.path.join(str(tmp_path), "ovf-env.xml") with open(ovf_path, "wb") as stream: stream.write(b"invalid xml") - with self.assertRaises( - errors.ReportableErrorOvfParsingException - ) as context_manager: - dsaz.load_azure_ds_dir(self.source_dir) - self.assertEqual( - "error parsing ovf-env.xml: syntax error: line 1, column 0", - context_manager.exception.reason, + with pytest.raises(errors.ReportableErrorOvfParsingException) as cm: + dsaz.load_azure_ds_dir(str(tmp_path)) + assert ( + "error parsing ovf-env.xml: syntax error: line 1, column 0" + == cm.value.reason ) -class TestReadAzureOvf(CiTestCase): +class TestReadAzureOvf: def test_invalid_xml_raises_non_azure_ds(self): invalid_xml = "" + construct_ovf_env() - self.assertRaises( - errors.ReportableErrorOvfParsingException, - dsaz.read_azure_ovf, - invalid_xml, - ) + with pytest.raises(errors.ReportableErrorOvfParsingException): + dsaz.read_azure_ovf( + invalid_xml, + ) def test_load_with_pubkeys(self): public_keys = [{"fingerprint": "fp1", "path": "path1", "value": ""}] content = construct_ovf_env(public_keys=public_keys) (_md, _ud, cfg) = dsaz.read_azure_ovf(content) for pk in public_keys: - self.assertIn(pk, cfg["_pubkeys"]) + assert pk in cfg["_pubkeys"] -class TestCanDevBeReformatted(CiTestCase): - with_logs = True +class TestCanDevBeReformatted: warning_file = "dataloss_warning_readme.txt" - def _domock(self, mockpath, sattr=None): - patcher = mock.patch(mockpath) - setattr(self, sattr, patcher.start()) - self.addCleanup(patcher.stop) - - def patchup(self, devs): - bypath = {} - for path, data in devs.items(): - bypath[path] = data - if "realpath" in data: - bypath[data["realpath"]] = data - for ppath, pdata in data.get("partitions", {}).items(): - bypath[ppath] = pdata + @pytest.fixture + def patchup(self, mocker): + def _patchup(devs): + bypath = {} + for path, data in devs.items(): + bypath[path] = data if "realpath" in data: - bypath[pdata["realpath"]] = pdata - - def realpath(d): - return bypath[d].get("realpath", d) - - def partitions_on_device(devpath): - parts = bypath.get(devpath, {}).get("partitions", {}) - ret = [] - for path, data in parts.items(): - ret.append((data.get("num"), realpath(path))) - # return sorted by partition number - return sorted(ret, key=lambda d: d[0]) - - def has_ntfs_fs(device): - return bypath.get(device, {}).get("fs") == "ntfs" - - p = MOCKPATH - self._domock(p + "_partitions_on_device", "m_partitions_on_device") - self._domock(p + "_has_ntfs_filesystem", "m_has_ntfs_filesystem") - self._domock(p + "os.path.realpath", "m_realpath") - self._domock(p + "os.path.exists", "m_exists") - self._domock(p + "util.SeLinuxGuard", "m_selguard") - - self.m_exists.side_effect = lambda p: p in bypath - self.m_realpath.side_effect = realpath - self.m_has_ntfs_filesystem.side_effect = has_ntfs_fs - self.m_partitions_on_device.side_effect = partitions_on_device - self.m_selguard.__enter__ = mock.Mock(return_value=False) - self.m_selguard.__exit__ = mock.Mock() - - return bypath + bypath[data["realpath"]] = data + for ppath, pdata in data.get("partitions", {}).items(): + bypath[ppath] = pdata + if "realpath" in data: + bypath[pdata["realpath"]] = pdata + + def realpath(d): + return bypath[d].get("realpath", d) + + def partitions_on_device(devpath): + parts = bypath.get(devpath, {}).get("partitions", {}) + ret = [] + for path, data in parts.items(): + ret.append((data.get("num"), realpath(path))) + # return sorted by partition number + return sorted(ret, key=lambda d: d[0]) + + def has_ntfs_fs(device): + return bypath.get(device, {}).get("fs") == "ntfs" + + p = MOCKPATH + self.m_partitions_on_device = mocker.patch( + p + "_partitions_on_device" + ) + self.m_has_ntfs_filesystem = mocker.patch( + p + "_has_ntfs_filesystem" + ) + self.m_realpath = mocker.patch(p + "os.path.realpath") + self.m_exists = mocker.patch(p + "os.path.exists") + self.m_selguard = mocker.patch(p + "util.SeLinuxGuard") + + self.m_exists.side_effect = lambda p: p in bypath + self.m_realpath.side_effect = realpath + self.m_has_ntfs_filesystem.side_effect = has_ntfs_fs + self.m_partitions_on_device.side_effect = partitions_on_device + self.m_selguard.__enter__ = mock.Mock(return_value=False) + self.m_selguard.__exit__ = mock.Mock() + + return bypath + + return _patchup + + @pytest.fixture + def domock_mount_cb(self, mocker, tmp_path): + def _do_mock_mount_cb(bypath): + def mount_cb( + device, callback, mtype, update_env_for_mount, log_error=False + ): + assert "ntfs" == mtype + assert "C" == update_env_for_mount.get("LANG") + for f in bypath.get(device).get("files", []): + write_file(os.path.join(tmp_path, f), content=f) + return callback(str(tmp_path)) + + p = MOCKPATH + self.m_mount_cb = mocker.patch(p + "util.mount_cb") + self.m_mount_cb.side_effect = mount_cb + + return _do_mock_mount_cb M_PATH = "cloudinit.util." @mock.patch(M_PATH + "subp.subp") - def test_ntfs_mount_logs(self, m_subp): + def test_ntfs_mount_logs(self, m_subp, caplog, patchup): """can_dev_be_reformatted does not log errors in case of unknown filesystem 'ntfs'.""" - self.patchup( + patchup( { "/dev/sda": { "partitions": { @@ -2488,26 +2487,11 @@ def test_ntfs_mount_logs(self, m_subp): ) dsaz.can_dev_be_reformatted("/dev/sda", preserve_ntfs=False) - self.assertNotIn(log_msg, self.logs.getvalue()) + assert log_msg not in caplog.text - def _domock_mount_cb(self, bypath): - def mount_cb( - device, callback, mtype, update_env_for_mount, log_error=False - ): - self.assertEqual("ntfs", mtype) - self.assertEqual("C", update_env_for_mount.get("LANG")) - p = self.tmp_dir() - for f in bypath.get(device).get("files", []): - write_file(os.path.join(p, f), content=f) - return callback(p) - - p = MOCKPATH - self._domock(p + "util.mount_cb", "m_mount_cb") - self.m_mount_cb.side_effect = mount_cb - - def test_three_partitions_is_false(self): + def test_three_partitions_is_false(self, domock_mount_cb, patchup): """A disk with 3 partitions can not be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2518,26 +2502,26 @@ def test_three_partitions_is_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertFalse(value) - self.assertIn("3 or more", msg.lower()) + assert not value + assert "3 or more" in msg.lower() - def test_no_partitions_is_false(self): + def test_no_partitions_is_false(self, patchup, domock_mount_cb): """A disk with no partitions can not be formatted.""" - bypath = self.patchup({"/dev/sda": {}}) - self._domock_mount_cb(bypath) + bypath = patchup({"/dev/sda": {}}) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertFalse(value) - self.assertIn("not partitioned", msg.lower()) + assert not value + assert "not partitioned" in msg.lower() - def test_two_partitions_not_ntfs_false(self): + def test_two_partitions_not_ntfs_false(self, patchup, domock_mount_cb): """2 partitions and 2nd not ntfs can not be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2547,16 +2531,18 @@ def test_two_partitions_not_ntfs_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertFalse(value) - self.assertIn("not ntfs", msg.lower()) + assert not value + assert "not ntfs" in msg.lower() - def test_two_partitions_ntfs_populated_false(self): + def test_two_partitions_ntfs_populated_false( + self, patchup, domock_mount_cb + ): """2 partitions and populated ntfs fs on 2nd can not be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2570,16 +2556,16 @@ def test_two_partitions_ntfs_populated_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertFalse(value) - self.assertIn("files on it", msg.lower()) + assert not value + assert "files on it" in msg.lower() - def test_two_partitions_ntfs_empty_is_true(self): + def test_two_partitions_ntfs_empty_is_true(self, patchup, domock_mount_cb): """2 partitions and empty ntfs fs on 2nd can be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2589,16 +2575,16 @@ def test_two_partitions_ntfs_empty_is_true(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertTrue(value) - self.assertIn("safe for", msg.lower()) + assert value + assert "safe for" in msg.lower() - def test_one_partition_not_ntfs_false(self): + def test_one_partition_not_ntfs_false(self, patchup, domock_mount_cb): """1 partition witih fs other than ntfs can not be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2607,16 +2593,18 @@ def test_one_partition_not_ntfs_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertFalse(value) - self.assertIn("not ntfs", msg.lower()) + assert not value + assert "not ntfs" in msg.lower() - def test_one_partition_ntfs_populated_false(self): + def test_one_partition_ntfs_populated_false( + self, patchup, domock_mount_cb + ): """1 mountable ntfs partition with many files can not be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2629,21 +2617,19 @@ def test_one_partition_ntfs_populated_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) with mock.patch.object(dsaz.LOG, "warning") as warning: value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) wmsg = warning.call_args[0][0] - self.assertIn( - "looks like you're using NTFS on the ephemeral disk", wmsg - ) - self.assertFalse(value) - self.assertIn("files on it", msg.lower()) + assert "looks like you're using NTFS on the ephemeral disk" in wmsg + assert not value + assert "files on it" in msg.lower() - def test_one_partition_ntfs_empty_is_true(self): + def test_one_partition_ntfs_empty_is_true(self, patchup, domock_mount_cb): """1 mountable ntfs partition and no files can be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2652,16 +2638,18 @@ def test_one_partition_ntfs_empty_is_true(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertTrue(value) - self.assertIn("safe for", msg.lower()) + assert value + assert "safe for" in msg.lower() - def test_one_partition_ntfs_empty_with_dataloss_file_is_true(self): + def test_one_partition_ntfs_empty_with_dataloss_file_is_true( + self, patchup, domock_mount_cb + ): """1 mountable ntfs partition and only warn file can be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2674,16 +2662,18 @@ def test_one_partition_ntfs_empty_with_dataloss_file_is_true(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertTrue(value) - self.assertIn("safe for", msg.lower()) + assert value + assert "safe for" in msg.lower() - def test_one_partition_ntfs_empty_with_svi_file_is_true(self): + def test_one_partition_ntfs_empty_with_svi_file_is_true( + self, patchup, domock_mount_cb + ): """1 mountable ntfs partition and only warn file can be formatted.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2696,17 +2686,19 @@ def test_one_partition_ntfs_empty_with_svi_file_is_true(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertTrue(value) - self.assertIn("safe for", msg.lower()) + assert value + assert "safe for" in msg.lower() - def test_one_partition_through_realpath_is_true(self): + def test_one_partition_through_realpath_is_true( + self, patchup, domock_mount_cb + ): """A symlink to a device with 1 ntfs partition can be formatted.""" epath = "/dev/disk/cloud/azure_resource" - bypath = self.patchup( + bypath = patchup( { epath: { "realpath": "/dev/sdb", @@ -2722,15 +2714,17 @@ def test_one_partition_through_realpath_is_true(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted(epath, preserve_ntfs=False) - self.assertTrue(value) - self.assertIn("safe for", msg.lower()) + assert value + assert "safe for" in msg.lower() - def test_three_partition_through_realpath_is_false(self): + def test_three_partition_through_realpath_is_false( + self, patchup, domock_mount_cb + ): """A symlink to a device with 3 partitions can not be formatted.""" epath = "/dev/disk/cloud/azure_resource" - bypath = self.patchup( + bypath = patchup( { epath: { "realpath": "/dev/sdb", @@ -2758,14 +2752,14 @@ def test_three_partition_through_realpath_is_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted(epath, preserve_ntfs=False) - self.assertFalse(value) - self.assertIn("3 or more", msg.lower()) + assert not value + assert "3 or more" in msg.lower() - def test_ntfs_mount_errors_true(self): + def test_ntfs_mount_errors_true(self, patchup, domock_mount_cb): """can_dev_be_reformatted does not fail if NTFS is unknown fstype.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2774,7 +2768,7 @@ def test_ntfs_mount_errors_true(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) error_msgs = [ "Stderr: mount: unknown filesystem type 'ntfs'", # RHEL @@ -2790,12 +2784,12 @@ def test_ntfs_mount_errors_true(self): value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=False ) - self.assertTrue(value) - self.assertIn("cannot mount NTFS, assuming", msg) + assert value + assert "cannot mount NTFS, assuming" in msg - def test_never_destroy_ntfs_config_false(self): + def test_never_destroy_ntfs_config_false(self, patchup, domock_mount_cb): """Normally formattable situation with never_destroy_ntfs set.""" - bypath = self.patchup( + bypath = patchup( { "/dev/sda": { "partitions": { @@ -2808,23 +2802,20 @@ def test_never_destroy_ntfs_config_false(self): } } ) - self._domock_mount_cb(bypath) + domock_mount_cb(bypath) value, msg = dsaz.can_dev_be_reformatted( "/dev/sda", preserve_ntfs=True ) - self.assertFalse(value) - self.assertIn( + assert not value + assert ( "config says to never destroy NTFS " - "(datasource.Azure.never_destroy_ntfs)", - msg, + "(datasource.Azure.never_destroy_ntfs)" in msg ) -class TestClearCachedData(CiTestCase): - def test_clear_cached_attrs_clears_imds(self): +class TestClearCachedData: + def test_clear_cached_attrs_clears_imds(self, paths): """All class attributes are reset to defaults, including imds data.""" - tmp = self.tmp_dir() - paths = helpers.Paths({"cloud_dir": tmp, "run_dir": tmp}) dsrc = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=paths) clean_values = [dsrc.metadata, dsrc.userdata, dsrc._metadata_imds] dsrc.metadata = "md" @@ -2832,26 +2823,28 @@ def test_clear_cached_attrs_clears_imds(self): dsrc._metadata_imds = "imds" dsrc._dirty_cache = True dsrc.clear_cached_attrs() - self.assertEqual( - [dsrc.metadata, dsrc.userdata, dsrc._metadata_imds], clean_values - ) + assert [ + dsrc.metadata, + dsrc.userdata, + dsrc._metadata_imds, + ] == clean_values -class TestAzureNetExists(CiTestCase): +class TestAzureNetExists: def test_azure_net_must_exist_for_legacy_objpkl(self): """DataSourceAzureNet must exist for old obj.pkl files that reference it.""" - self.assertTrue(hasattr(dsaz, "DataSourceAzureNet")) + assert hasattr(dsaz, "DataSourceAzureNet") -class TestPreprovisioningReadAzureOvfFlag(CiTestCase): +class TestPreprovisioningReadAzureOvfFlag: def test_read_azure_ovf_with_true_flag(self): """The read_azure_ovf method should set the PreprovisionedVM cfg flag if the proper setting is present.""" content = construct_ovf_env(preprovisioned_vm=True) ret = dsaz.read_azure_ovf(content) cfg = ret[2] - self.assertTrue(cfg["PreprovisionedVm"]) + assert cfg["PreprovisionedVm"] def test_read_azure_ovf_with_false_flag(self): """The read_azure_ovf method should set the PreprovisionedVM @@ -2859,7 +2852,7 @@ def test_read_azure_ovf_with_false_flag(self): content = construct_ovf_env(preprovisioned_vm=False) ret = dsaz.read_azure_ovf(content) cfg = ret[2] - self.assertFalse(cfg["PreprovisionedVm"]) + assert not cfg["PreprovisionedVm"] def test_read_azure_ovf_without_flag(self): """The read_azure_ovf method should not set the @@ -2867,8 +2860,8 @@ def test_read_azure_ovf_without_flag(self): content = construct_ovf_env() ret = dsaz.read_azure_ovf(content) cfg = ret[2] - self.assertFalse(cfg["PreprovisionedVm"]) - self.assertEqual(None, cfg["PreprovisionedVMType"]) + assert not cfg["PreprovisionedVm"] + assert None is cfg["PreprovisionedVMType"] def test_read_azure_ovf_with_running_type(self): """The read_azure_ovf method should set PreprovisionedVMType @@ -2878,8 +2871,8 @@ def test_read_azure_ovf_with_running_type(self): ) ret = dsaz.read_azure_ovf(content) cfg = ret[2] - self.assertTrue(cfg["PreprovisionedVm"]) - self.assertEqual("Running", cfg["PreprovisionedVMType"]) + assert cfg["PreprovisionedVm"] + assert "Running" == cfg["PreprovisionedVMType"] def test_read_azure_ovf_with_savable_type(self): """The read_azure_ovf method should set PreprovisionedVMType @@ -2889,8 +2882,8 @@ def test_read_azure_ovf_with_savable_type(self): ) ret = dsaz.read_azure_ovf(content) cfg = ret[2] - self.assertTrue(cfg["PreprovisionedVm"]) - self.assertEqual("Savable", cfg["PreprovisionedVMType"]) + assert cfg["PreprovisionedVm"] + assert "Savable" == cfg["PreprovisionedVMType"] def test_read_azure_ovf_with_proxy_guest_agent_true(self): """The read_azure_ovf method should set ProvisionGuestProxyAgent @@ -2989,14 +2982,10 @@ def test_determine_pps_with_reprovision_marker( ] -class TestPreprovisioningHotAttachNics(CiTestCase): - def setUp(self): - super(TestPreprovisioningHotAttachNics, self).setUp() - self.tmp = self.tmp_dir() - self.waagent_d = self.tmp_path("/var/lib/waagent", self.tmp) - self.paths = helpers.Paths({"cloud_dir": self.tmp}) - dsaz.BUILTIN_DS_CONFIG["data_dir"] = self.waagent_d - self.paths = helpers.Paths({"cloud_dir": self.tmp}) +class TestPreprovisioningHotAttachNics: + @pytest.fixture(autouse=True) + def fixtures(self, waagent_d): + dsaz.BUILTIN_DS_CONFIG["data_dir"] = waagent_d @mock.patch(MOCKPATH + "util.write_file", autospec=True) @mock.patch(MOCKPATH + "DataSourceAzure._report_ready") @@ -3010,14 +2999,15 @@ def test_detect_nic_attach_reports_ready_and_waits_for_detach( m_wait_for_hot_attached_primary_nic, m_report_ready, m_writefile, + paths, ): """Report ready first and then wait for nic detach""" - dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths) + dsa = dsaz.DataSourceAzure({}, distro=None, paths=paths) dsa._wait_for_pps_savable_reuse() - self.assertEqual(1, m_report_ready.call_count) - self.assertEqual(1, m_wait_for_hot_attached_primary_nic.call_count) - self.assertEqual(1, m_detach.call_count) - self.assertEqual(1, m_writefile.call_count) + assert 1 == m_report_ready.call_count + assert 1 == m_wait_for_hot_attached_primary_nic.call_count + assert 1 == m_detach.call_count + assert 1 == m_writefile.call_count m_writefile.assert_called_with( dsa._reported_ready_marker_file, mock.ANY ) @@ -3038,12 +3028,14 @@ def test_wait_for_nic_attach_multinic_attach( m_link_up, m_report_ready, m_writefile, + paths, + tmp_path, ): """Wait for nic attach if we do not have a fallback interface. Skip waiting for additional nics after we have found primary""" distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths) + distro.get_tmp_exec_path = str(tmp_path) + dsa = dsaz.DataSourceAzure({}, distro=distro, paths=paths) lease = { "interface": "eth9", "fixed-address": "192.168.2.9", @@ -3078,13 +3070,13 @@ def test_wait_for_nic_attach_multinic_attach( dsa._wait_for_pps_savable_reuse() - self.assertEqual(1, m_detach.call_count) + assert 1 == m_detach.call_count # only wait for primary nic - self.assertEqual(1, m_attach.call_count) + assert 1 == m_attach.call_count # DHCP and network metadata calls will only happen on the primary NIC. - self.assertEqual(1, m_dhcpv4.call_count) + assert 1 == m_dhcpv4.call_count # no call to bring link up on secondary nic - self.assertEqual(1, m_link_up.call_count) + assert 1 == m_link_up.call_count # reset mock to test again with primary nic being eth1 dhcp_ctx_primary.interface = "eth1" @@ -3106,59 +3098,60 @@ def test_wait_for_nic_attach_multinic_attach( m_dhcpv4.side_effect = [dhcp_ctx_secondary, dhcp_ctx_primary] m_link_up.reset_mock() m_attach.side_effect = ["eth0", "eth1"] - dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths) + dsa = dsaz.DataSourceAzure({}, distro=distro, paths=paths) dsa._wait_for_pps_savable_reuse() - self.assertEqual(1, m_detach.call_count) - self.assertEqual(2, m_attach.call_count) - self.assertEqual(2, m_dhcpv4.call_count) - self.assertEqual(2, m_link_up.call_count) + assert 1 == m_detach.call_count + assert 2 == m_attach.call_count + assert 2 == m_dhcpv4.call_count + assert 2 == m_link_up.call_count @mock.patch("cloudinit.distros.networking.LinuxNetworking.try_set_link_up") - def test_wait_for_link_up_returns_if_already_up(self, m_is_link_up): + def test_wait_for_link_up_returns_if_already_up(self, m_is_link_up, paths): """Waiting for link to be up should return immediately if the link is already up.""" distro_cls = distros.fetch("ubuntu") - distro = distro_cls("ubuntu", {}, self.paths) - dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths) + distro = distro_cls("ubuntu", {}, paths) + dsa = dsaz.DataSourceAzure({}, distro=distro, paths=paths) m_is_link_up.return_value = True dsa.wait_for_link_up("eth0") - self.assertEqual(1, m_is_link_up.call_count) + assert 1 == m_is_link_up.call_count @mock.patch("cloudinit.distros.networking.LinuxNetworking.try_set_link_up") @mock.patch(MOCKPATH + "sleep") def test_wait_for_link_up_checks_link_after_sleep( - self, m_sleep, m_try_set_link_up + self, m_sleep, m_try_set_link_up, paths ): """Waiting for link to be up should return immediately if the link is already up.""" distro_cls = distros.fetch("ubuntu") - distro = distro_cls("ubuntu", {}, self.paths) - dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths) + distro = distro_cls("ubuntu", {}, paths) + dsa = dsaz.DataSourceAzure({}, distro=distro, paths=paths) m_try_set_link_up.return_value = False dsa.wait_for_link_up("eth0") - self.assertEqual(100, m_try_set_link_up.call_count) - self.assertEqual(99 * [mock.call(0.1)], m_sleep.mock_calls) + assert 100 == m_try_set_link_up.call_count + assert 99 * [mock.call(0.1)] == m_sleep.mock_calls @mock.patch( "cloudinit.sources.helpers.netlink.create_bound_netlink_socket" ) - def test_wait_for_all_nics_ready_raises_if_socket_fails(self, m_socket): + def test_wait_for_all_nics_ready_raises_if_socket_fails( + self, m_socket, paths + ): """Waiting for all nics should raise exception if netlink socket creation fails.""" m_socket.side_effect = netlink.NetlinkCreateSocketError distro_cls = distros.fetch("ubuntu") - distro = distro_cls("ubuntu", {}, self.paths) - dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths) + distro = distro_cls("ubuntu", {}, paths) + dsa = dsaz.DataSourceAzure({}, distro=distro, paths=paths) - self.assertRaises( - netlink.NetlinkCreateSocketError, dsa._wait_for_pps_savable_reuse - ) + with pytest.raises(netlink.NetlinkCreateSocketError): + dsa._wait_for_pps_savable_reuse() @mock.patch("cloudinit.net.find_fallback_nic", return_value="eth9") @@ -3167,13 +3160,10 @@ def test_wait_for_all_nics_ready_raises_if_socket_fails(self, m_socket): "cloudinit.sources.helpers.netlink.wait_for_media_disconnect_connect" ) @mock.patch(MOCKPATH + "imds.fetch_reprovision_data") -class TestPreprovisioningPollIMDS(CiTestCase): - def setUp(self): - super(TestPreprovisioningPollIMDS, self).setUp() - self.tmp = self.tmp_dir() - self.waagent_d = self.tmp_path("/var/lib/waagent", self.tmp) - self.paths = helpers.Paths({"cloud_dir": self.tmp}) - dsaz.BUILTIN_DS_CONFIG["data_dir"] = self.waagent_d +class TestPreprovisioningPollIMDS: + @pytest.fixture(autouse=True) + def fixtures(self, waagent_d): + dsaz.BUILTIN_DS_CONFIG["data_dir"] = waagent_d @mock.patch("time.sleep", mock.MagicMock()) def test_poll_imds_re_dhcp_on_timeout( @@ -3182,6 +3172,7 @@ def test_poll_imds_re_dhcp_on_timeout( m_media_switch, m_dhcp, m_fallback, + paths, ): """The poll_imds will retry DHCP on IMDS timeout.""" m_fetch_reprovisiondata.side_effect = [ @@ -3201,11 +3192,11 @@ def test_poll_imds_re_dhcp_on_timeout( dhcp_ctx.obtain_lease.return_value = lease dhcp_ctx.iface = lease["interface"] - dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths) + dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=paths) dsa._ephemeral_dhcp_ctx = dhcp_ctx dsa._poll_imds() - self.assertEqual(1, m_dhcp.call_count, "Expected 1 DHCP calls") + assert 1 == m_dhcp.call_count, "Expected 1 DHCP calls" assert m_fetch_reprovisiondata.call_count == 2 @mock.patch("os.path.isfile") @@ -3216,6 +3207,7 @@ def test_poll_imds_skips_dhcp_if_ctx_present( m_media_switch, m_dhcp, m_fallback, + paths, ): """The poll_imds function should reuse the dhcp ctx if it is already present. This happens when we wait for nic to be hot-attached before @@ -3223,11 +3215,11 @@ def test_poll_imds_skips_dhcp_if_ctx_present( _poll_imds is called, then it is not expected to be waiting for media_disconnect_connect either.""" m_isfile.return_value = True - dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths) + dsa = dsaz.DataSourceAzure({}, distro=None, paths=paths) dsa._ephemeral_dhcp_ctx = mock.Mock(lease={}) dsa._poll_imds() - self.assertEqual(0, m_dhcp.call_count) - self.assertEqual(0, m_media_switch.call_count) + assert 0 == m_dhcp.call_count + assert 0 == m_media_switch.call_count @mock.patch("os.path.isfile") def test_poll_imds_does_dhcp_on_retries_if_ctx_present( @@ -3237,6 +3229,8 @@ def test_poll_imds_does_dhcp_on_retries_if_ctx_present( m_media_switch, m_dhcp, m_fallback, + paths, + tmp_path, ): """The poll_imds function should reuse the dhcp ctx if it is already present. This happens when we wait for nic to be hot-attached before @@ -3254,70 +3248,69 @@ def test_poll_imds_does_dhcp_on_retries_if_ctx_present( ] m_isfile.return_value = True distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths) + distro.get_tmp_exec_path = str(tmp_path) + dsa = dsaz.DataSourceAzure({}, distro=distro, paths=paths) with mock.patch.object(dsa, "_ephemeral_dhcp_ctx") as m_dhcp_ctx: m_dhcp_ctx.obtain_lease.return_value = "Dummy lease" dsa._ephemeral_dhcp_ctx = m_dhcp_ctx dsa._poll_imds() - self.assertEqual(1, m_dhcp_ctx.clean_network.call_count) - self.assertEqual(1, m_dhcp.call_count) - self.assertEqual(0, m_media_switch.call_count) - self.assertEqual(2, m_fetch_reprovisiondata.call_count) - + assert 1 == m_dhcp_ctx.clean_network.call_count + assert 1 == m_dhcp.call_count + assert 0 == m_media_switch.call_count + assert 2 == m_fetch_reprovisiondata.call_count -class TestRemoveUbuntuNetworkConfigScripts(CiTestCase): - with_logs = True - def setUp(self): - super(TestRemoveUbuntuNetworkConfigScripts, self).setUp() - self.tmp = self.tmp_dir() - - def test_remove_network_scripts_removes_both_files_and_directories(self): +class TestRemoveUbuntuNetworkConfigScripts: + def test_remove_network_scripts_removes_both_files_and_directories( + self, caplog, tmp_path + ): """Any files or directories in paths are removed when present.""" - file1 = self.tmp_path("file1", dir=self.tmp) - subdir = self.tmp_path("sub1", dir=self.tmp) - subfile = self.tmp_path("leaf1", dir=subdir) + file1 = tmp_path / "file1" + subdir = tmp_path / "sub1" + subfile = subdir / "leaf1" write_file(file1, "file1content") write_file(subfile, "leafcontent") dsaz.maybe_remove_ubuntu_network_config_scripts(paths=[subdir, file1]) for path in (file1, subdir, subfile): - self.assertFalse( - os.path.exists(path), "Found unremoved: %s" % path - ) + assert not os.path.exists(path), "Found unremoved: %s" % path expected_logs = [ - "INFO: Removing Ubuntu extended network scripts because cloud-init" - " updates Azure network configuration on the following events:" - " ['boot', 'boot-legacy']", - "Recursively deleting %s" % subdir, - "Attempting to remove %s" % file1, + ( + mock.ANY, + logging.INFO, + "Removing Ubuntu extended network scripts because cloud-init" + " updates Azure network configuration on the following events:" + " ['boot', 'boot-legacy'].", + ), + (mock.ANY, logging.DEBUG, "Recursively deleting %s" % subdir), + (mock.ANY, logging.DEBUG, "Attempting to remove %s" % file1), ] for log in expected_logs: - self.assertIn(log, self.logs.getvalue()) + assert log in caplog.record_tuples - def test_remove_network_scripts_only_attempts_removal_if_path_exists(self): + def test_remove_network_scripts_only_attempts_removal_if_path_exists( + self, caplog, tmp_path + ): """Any files or directories absent are skipped without error.""" dsaz.maybe_remove_ubuntu_network_config_scripts( paths=[ - self.tmp_path("nodirhere/", dir=self.tmp), - self.tmp_path("notfilehere", dir=self.tmp), + tmp_path / "nodirhere/", + tmp_path / "notfilehere", ] ) - self.assertNotIn("/not/a", self.logs.getvalue()) # No delete logs + assert "/not/a" not in caplog.text # No delete logs - @mock.patch(MOCKPATH + "os.path.exists") + # Report path absent on all to avoid delete operation + @mock.patch(MOCKPATH + "os.path.exists", return_value=False) def test_remove_network_scripts_default_removes_stock_scripts( self, m_exists ): """Azure's stock ubuntu image scripts and artifacts are removed.""" - # Report path absent on all to avoid delete operation - m_exists.return_value = False dsaz.maybe_remove_ubuntu_network_config_scripts() calls = m_exists.call_args_list for path in dsaz.UBUNTU_EXTENDED_NETWORK_SCRIPTS: - self.assertIn(mock.call(path), calls) + assert mock.call(path) in calls class TestIsPlatformViable: @@ -3361,7 +3354,7 @@ def test_false_on_no_matching_azure_criteria( ) -class TestRandomSeed(CiTestCase): +class TestRandomSeed: """Test proper handling of random_seed""" def test_non_ascii_seed_is_serializable(self): @@ -3377,9 +3370,9 @@ def test_non_ascii_seed_is_serializable(self): serialized = json_dumps(obj) deserialized = load_json(serialized) except UnicodeDecodeError: - self.fail("Non-serializable random seed returned") + pytest.fail("Non-serializable random seed returned") - self.assertEqual(deserialized["seed"], result) + assert deserialized["seed"] == result class TestEphemeralNetworking: From c4e80f7b623151c21dc396ab8c9ff98a87fb3a30 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Mon, 28 Jul 2025 22:14:59 -0500 Subject: [PATCH 038/150] ci: downgrade LXD to fix integration tests (#6340) --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 6cd248c7a0c..3debe84f173 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -56,7 +56,7 @@ jobs: - name: Setup LXD uses: canonical/setup-lxd@v0.1.2 with: - channel: 6/edge + channel: 6/stable - name: Verify deb package run: | ls -hal '${{ runner.temp }}' From 08630373f0e2e6a34e46b248a55180af645a8c81 Mon Sep 17 00:00:00 2001 From: abdulganiyy Date: Tue, 29 Jul 2025 15:32:36 +0100 Subject: [PATCH 039/150] fixed defs types in cloudinit analyze (#6308) --- cloudinit/analyze/__init__.py | 24 +++++++++++++----------- pyproject.toml | 1 - 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cloudinit/analyze/__init__.py b/cloudinit/analyze/__init__.py index 24dfa936142..60ff8daab16 100644 --- a/cloudinit/analyze/__init__.py +++ b/cloudinit/analyze/__init__.py @@ -6,13 +6,15 @@ import re import sys from datetime import datetime, timezone -from typing import IO +from typing import IO, Dict, List, Optional, Tuple, Union from cloudinit.analyze import dump, show from cloudinit.atomic_helper import json_dumps -def get_parser(parser=None): +def get_parser( + parser: Optional[argparse.ArgumentParser] = None, +) -> argparse.ArgumentParser: if not parser: parser = argparse.ArgumentParser( prog="cloudinit-analyze", @@ -113,7 +115,7 @@ def get_parser(parser=None): return parser -def analyze_boot(name, args): +def analyze_boot(name: str, args: argparse.Namespace) -> int: """Report a list of how long different boot operations took. For Example: @@ -138,10 +140,10 @@ def analyze_boot(name, args): e for e in _get_events(infh) if e["name"] == "init-local" - and "starting search" in e["description"] + and "starting search" in str(e["description"]) ][-1] - ci_start = datetime.fromtimestamp( - last_init_local["timestamp"], timezone.utc + ci_start: Union[datetime, str] = datetime.fromtimestamp( + float(last_init_local["timestamp"]), timezone.utc ) except IndexError: ci_start = "Could not find init-local log-line in cloud-init.log" @@ -200,7 +202,7 @@ def analyze_boot(name, args): return status_code -def analyze_blame(name, args): +def analyze_blame(name, args: argparse.Namespace) -> None: """Report a list of records sorted by largest time delta. For example: @@ -227,7 +229,7 @@ def analyze_blame(name, args): clean_io(infh, outfh) -def analyze_show(name, args): +def analyze_show(name, args: argparse.Namespace) -> None: """Generate output records using the 'standard' format to printing events. Example output follows: @@ -264,14 +266,14 @@ def analyze_show(name, args): clean_io(infh, outfh) -def analyze_dump(name, args): +def analyze_dump(name, args: argparse.Namespace) -> None: """Dump cloud-init events in json format""" infh, outfh = configure_io(args) outfh.write(json_dumps(_get_events(infh)) + "\n") clean_io(infh, outfh) -def _get_events(infile): +def _get_events(infile: IO) -> List[Dict[str, Union[str, float]]]: rawdata = None events, rawdata = show.load_events_infile(infile) if not events: @@ -279,7 +281,7 @@ def _get_events(infile): return events -def configure_io(args): +def configure_io(args: argparse.Namespace) -> Tuple[IO, IO]: """Common parsing and setup of input/output files""" if args.infile == "-": infh = sys.stdin diff --git a/pyproject.toml b/pyproject.toml index c6424a29b18..8593894e95e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ no_implicit_optional = true # See GH-5445 [[tool.mypy.overrides]] module = [ - "cloudinit.analyze", "cloudinit.analyze.dump", "cloudinit.analyze.show", "cloudinit.cmd.devel.make_mime", From 5e11d38db7cdcfb02394aded9adacd0029bc078d Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Wed, 9 Jul 2025 16:59:17 -0600 Subject: [PATCH 040/150] doc: Document changed systemd order in 24.4 --- doc/rtd/reference/breaking_changes.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/rtd/reference/breaking_changes.rst b/doc/rtd/reference/breaking_changes.rst index bafb33f497a..1f5d613ccd8 100644 --- a/doc/rtd/reference/breaking_changes.rst +++ b/doc/rtd/reference/breaking_changes.rst @@ -101,7 +101,15 @@ anything that was installed to ``/lib`` is now installed to ``/usr/lib``. This shouldn't affect any systemd-based distributions as they have all transitioned to the ``/usr`` merge. However, this could affect older stable releases, non-systemd and non-Linux distributions. See -`this commit `_ +`commit 054734921 `_ +for more details. + +24.4 +==== + +Cloud-init's `cloud-final.service` order was standardized. This caused a +change to the systemd boot order on some distributions. See +`commit 245f94674 `_ for more details. 24.3 From 60d40baa2f94ce9a28f13425114692a0bbc865ca Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Wed, 9 Jul 2025 17:19:43 -0600 Subject: [PATCH 041/150] doc: Document how to wait for cloud-init --- doc/rtd/howto/index.rst | 1 + doc/rtd/howto/wait_for_cloud_init.rst | 33 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 doc/rtd/howto/wait_for_cloud_init.rst diff --git a/doc/rtd/howto/index.rst b/doc/rtd/howto/index.rst index d3bc25524bf..d03e3909f66 100644 --- a/doc/rtd/howto/index.rst +++ b/doc/rtd/howto/index.rst @@ -19,6 +19,7 @@ How do I...? :maxdepth: 1 Launch cloud-init with... + Wait for cloud-init Re-run cloud-init Change how often a module runs Validate my user-data diff --git a/doc/rtd/howto/wait_for_cloud_init.rst b/doc/rtd/howto/wait_for_cloud_init.rst new file mode 100644 index 00000000000..929ab138593 --- /dev/null +++ b/doc/rtd/howto/wait_for_cloud_init.rst @@ -0,0 +1,33 @@ +.. _wait_for_cloud_init: + +How to wait for cloud-init +************************** + +It is useful to be able to wait until cloud-init has completed running prior +to doing some other task. + +CLI +=== + +Cloud-init's command ``cloud-init status --wait`` will exit once cloud-init has +completed. + +SystemD +======= + +Systems using systemd may be configured to start a service after cloud-init +completes. This may be accomplished by including +``After=cloud-init.target multi-user.target`` in the unit file. For example: + +.. code-block:: + + [Unit] + Description=Example service + After=cloud-final.service multi-user.target + + [Service] + Type=oneshot + ExecStart=sh -c 'echo "Howdy partner 🤠"' + + [Install] + WantedBy=multi-user.target From cf5bb7b08d0db14fb09b15ec1dc8d63c72bb0af2 Mon Sep 17 00:00:00 2001 From: a-dubs Date: Thu, 17 Jul 2025 10:05:47 -0400 Subject: [PATCH 042/150] feat(network): add keep_configuration setting to v1 This config option is only implemented for the Netplan renderer. It is used to designate the connection as 'critical to the system', meaning that special care will be taken not to release the assigned IP when the daemon is restarted. PR: 6097 --- cloudinit/config/schemas/schema-network-config-v1.json | 4 ++++ cloudinit/net/netplan.py | 2 ++ cloudinit/net/network_state.py | 1 + doc/rtd/reference/network-config-format-v1.rst | 9 +++++++++ 4 files changed, 16 insertions(+) diff --git a/cloudinit/config/schemas/schema-network-config-v1.json b/cloudinit/config/schemas/schema-network-config-v1.json index 38270e98235..92ded462706 100644 --- a/cloudinit/config/schemas/schema-network-config-v1.json +++ b/cloudinit/config/schemas/schema-network-config-v1.json @@ -40,6 +40,10 @@ "accept-ra": { "type": "boolean", "description": "Whether to accept IPv6 Router Advertisements (RA) on this interface. If unset, it will not be rendered" + }, + "keep_configuration": { + "type": "boolean", + "description": "Designate the connection as 'critical to the system', meaning that special care will be taken not to release the assigned IP when the daemon is restarted. (only recognized by Netplan renderer)." } } }, diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index bce89bbd302..64a3c4786a3 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -481,6 +481,8 @@ def _render_content(self, network_state: NetworkState) -> str: "set-name": ifname, "match": ifcfg.get("match", None), } + if "keep_configuration" in ifcfg: + eth["critical"] = ifcfg["keep_configuration"] if eth["match"] is None: macaddr = ifcfg.get("mac_address", None) if macaddr is not None: diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index 2bdd2b614c0..9fd51e42916 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -428,6 +428,7 @@ def handle_physical(self, command): "accept-ra": accept_ra, "wakeonlan": wakeonlan, "optional": optional, + "keep_configuration": command.get("keep_configuration"), } ) iface_key = command.get("config_id", command.get("name")) diff --git a/doc/rtd/reference/network-config-format-v1.rst b/doc/rtd/reference/network-config-format-v1.rst index 3598b4b377f..5186075b093 100644 --- a/doc/rtd/reference/network-config-format-v1.rst +++ b/doc/rtd/reference/network-config-format-v1.rst @@ -95,6 +95,15 @@ Physical example .. literalinclude:: ../../examples/network-config-v1-physical-3-nic.yaml :language: yaml +``keep_configuration: `` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Designate the connection as 'critical to the system', meaning that special care +will be taken not to release the assigned IP when the daemon is restarted. + +.. note:: + This is only recognized by Netplan renderer. + Bond ---- From 0fec2e239b7769d5cfbbb0a3b03cc53b7fdef406 Mon Sep 17 00:00:00 2001 From: a-dubs Date: Thu, 17 Jul 2025 10:09:19 -0400 Subject: [PATCH 043/150] feat(oracle): set keep_configuration to true for iscsi instances This is necessary for Oracle Baremetal instances on IPv6-only networks which rely on the iscsi connection. Without this, after shutting down the instance, the instance is not recoverable. This setting will be automatically set for all iscsi instances via the Oracle Datasource based on metadata/config files left behind by initramfs during boot on ISCSI instances. PR: #6097 --- cloudinit/sources/DataSourceOracle.py | 100 +++++++----------- .../datasources/test_oci_networking.py | 47 ++++++++ tests/unittests/sources/test_oracle.py | 89 ---------------- 3 files changed, 88 insertions(+), 148 deletions(-) diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py index e4195f8f7f8..aa62aa2d2aa 100644 --- a/cloudinit/sources/DataSourceOracle.py +++ b/cloudinit/sources/DataSourceOracle.py @@ -293,9 +293,15 @@ def network_config(self): return self._network_config set_primary = False - # this is v1 if self._is_iscsi_root(): self._network_config = self._get_iscsi_config() + logging.debug( + "Instance is using iSCSI root, setting primary NIC as critical" + ) + # This is necessary for Oracle baremetal instances in case they are + # running on an IPv6-only network. Without this, they become + # unreachable/unrecoverable after a shutdown. + self._network_config["config"][0]["keep_configuration"] = True if not self._has_network_config(): LOG.debug( "Could not obtain network configuration from initramfs. " @@ -380,65 +386,41 @@ def _add_network_config_from_opc_imds(self, set_primary: bool = False): else: network = ipaddress.ip_network(vnic_dict["subnetCidrBlock"]) - if self._network_config["version"] == 1: - if is_primary: - if is_ipv6_only: - subnets = [{"type": "dhcp6"}] - else: - subnets = [{"type": "dhcp"}] + if is_primary: + if is_ipv6_only: + subnets = [{"type": "dhcp6"}] else: - subnets = [] - if vnic_dict.get("privateIp"): - subnets.append( - { - "type": "static", - "address": ( - f"{vnic_dict['privateIp']}/" - f"{network.prefixlen}" - ), - } - ) - if vnic_dict.get("ipv6Addresses"): - subnets.append( - { - "type": "static", - "address": ( - f"{vnic_dict['ipv6Addresses'][0]}/" - f"{network.prefixlen}" - ), - } - ) - interface_config = { - "name": name, - "type": "physical", - "mac_address": mac_address, - "mtu": MTU, - "subnets": subnets, - } - self._network_config["config"].append(interface_config) - elif self._network_config["version"] == 2: - # Why does this elif exist??? - # Are there plans to switch to v2? - interface_config = { - "mtu": MTU, - "match": {"macaddress": mac_address}, - } - self._network_config["ethernets"][name] = interface_config - - interface_config["dhcp6"] = is_primary and is_ipv6_only - interface_config["dhcp4"] = is_primary and not is_ipv6_only - if not is_primary: - interface_config["addresses"] = [] - if vnic_dict.get("privateIp"): - interface_config["addresses"].append( - f"{vnic_dict['privateIp']}/{network.prefixlen}" - ) - if vnic_dict.get("ipv6Addresses"): - interface_config["addresses"].append( - f"{vnic_dict['ipv6Addresses'][0]}/" - f"{network.prefixlen}" - ) - self._network_config["ethernets"][name] = interface_config + subnets = [{"type": "dhcp"}] + else: + subnets = [] + if vnic_dict.get("privateIp"): + subnets.append( + { + "type": "static", + "address": ( + f"{vnic_dict['privateIp']}/" + f"{network.prefixlen}" + ), + } + ) + if vnic_dict.get("ipv6Addresses"): + subnets.append( + { + "type": "static", + "address": ( + f"{vnic_dict['ipv6Addresses'][0]}/" + f"{network.prefixlen}" + ), + } + ) + interface_config = { + "name": name, + "type": "physical", + "mac_address": mac_address, + "mtu": MTU, + "subnets": subnets, + } + self._network_config["config"].append(interface_config) class DataSourceOracleNet(DataSourceOracle): diff --git a/tests/integration_tests/datasources/test_oci_networking.py b/tests/integration_tests/datasources/test_oci_networking.py index 9b807927668..f50d6b96cf1 100644 --- a/tests/integration_tests/datasources/test_oci_networking.py +++ b/tests/integration_tests/datasources/test_oci_networking.py @@ -157,3 +157,50 @@ def test_oci_networking_system_cfg(client: IntegrationInstance, tmpdir): netplan_cfg = yaml.safe_load(netplan_yaml) expected_netplan_cfg = yaml.safe_load(SYSTEM_CFG) assert expected_netplan_cfg == netplan_cfg + + +@pytest.mark.skipif(PLATFORM != "oci", reason="Test is OCI specific") +def test_oci_keep_configuration_networking_config( + session_cloud: IntegrationCloud, +): + """ + Test to ensure the keep_configuration is applied on Oracle ISCSI instances. + + This test launches a Baremetal OCI instance so that ISCSI is used, and + checks that the primary systemd network configuration file contains the + 'KeepConfiguration=true' directive, which indicates that the network + configuration is preserved as expected. + + Assertions: + - At least one netplan file exists under '/run/systemd/network'. + - The primary systemd network configuration file includes the + 'KeepConfiguration=true' directive. + - The netplan configuration includes the 'critical: true' directive. + """ + with session_cloud.launch( + launch_kwargs={ + "instance_type": "BM.Optimized3.36", + }, + ) as client: + r = client.execute("ls /run/systemd/network/10-netplan-*.network") + assert r.ok, ( + "No netplan files found under /run/systemd/network. We are looking" + " for netplan files here to check that the underlying " + "'KeepConfiguration=true' directive is actually being applied to " + "the systemd network configuration." + ) + primary_systemd_file: str = r.stdout.strip().splitlines()[0] + systemd_config = client.read_from_file(primary_systemd_file) + assert ( + "KeepConfiguration=true" in systemd_config + or "CriticalConnection=true" in systemd_config + ), ( + f"Neither 'KeepConfiguration=true' nor 'CriticalConnection=true' " + f"found in '{primary_systemd_file}':\n{primary_systemd_file}" + ) + netplan_config = client.read_from_file( + "/etc/netplan/50-cloud-init.yaml", + ) + assert ( + "critical: true" in netplan_config + ), "critical: true not found in netplan config" diff --git a/tests/unittests/sources/test_oracle.py b/tests/unittests/sources/test_oracle.py index efa492d1058..1a61510a3b2 100644 --- a/tests/unittests/sources/test_oracle.py +++ b/tests/unittests/sources/test_oracle.py @@ -485,48 +485,6 @@ def test_imds_nic_setup_v1(self, set_primary, oracle_ds): assert "10.0.0.231/24" == secondary_cfg["subnets"][0]["address"] assert "static" == secondary_cfg["subnets"][0]["type"] - @pytest.mark.parametrize( - "set_primary", - [True, False], - ) - def test_secondary_nic_v2(self, set_primary, oracle_ds): - oracle_ds._vnics_data = json.loads(OPC_VM_SECONDARY_VNIC_RESPONSE) - oracle_ds._network_config = { - "version": 2, - "ethernets": {"primary": {"nic": {}}}, - } - with mock.patch( - f"{DS_PATH}.get_interfaces_by_mac", - return_value={ - "02:00:17:05:d1:db": "ens3", - "00:00:17:02:2b:b1": "ens4", - }, - ): - oracle_ds._add_network_config_from_opc_imds( - set_primary=set_primary - ) - - nic_cfg = oracle_ds.network_config["ethernets"] - if set_primary: - assert "ens3" in nic_cfg - primary_cfg = nic_cfg["ens3"] - - assert primary_cfg["dhcp4"] is True - assert primary_cfg["dhcp6"] is False - assert "02:00:17:05:d1:db" == primary_cfg["match"]["macaddress"] - assert 9000 == primary_cfg["mtu"] - assert "addresses" not in primary_cfg - - assert "ens4" in nic_cfg - secondary_cfg = nic_cfg["ens4"] - assert secondary_cfg["dhcp4"] is False - assert secondary_cfg["dhcp6"] is False - assert "00:00:17:02:2b:b1" == secondary_cfg["match"]["macaddress"] - assert 9000 == secondary_cfg["mtu"] - - assert 1 == len(secondary_cfg["addresses"]) - assert "10.0.0.231/24" == secondary_cfg["addresses"][0] - @pytest.mark.parametrize( "set_primary", [ @@ -578,53 +536,6 @@ def test_imds_nic_setup_v1_ipv6_only(self, set_primary, oracle_ds): ) assert "static" == secondary_cfg["subnets"][0]["type"] - @pytest.mark.parametrize( - "set_primary", - [True, False], - ) - def test_secondary_nic_v2_ipv6_only(self, set_primary, oracle_ds): - oracle_ds._vnics_data = json.loads( - OPC_VM_IPV6_ONLY_SECONDARY_VNIC_RESPONSE - ) - oracle_ds._network_config = { - "version": 2, - "ethernets": {"primary": {"nic": {}}}, - } - with mock.patch( - f"{DS_PATH}.get_interfaces_by_mac", - return_value={ - "02:00:17:0d:6b:be": "ens3", - "02:00:17:18:f6:ff": "ens4", - }, - ): - oracle_ds._add_network_config_from_opc_imds( - set_primary=set_primary - ) - - nic_cfg = oracle_ds.network_config["ethernets"] - if set_primary: - assert "ens3" in nic_cfg - primary_cfg = nic_cfg["ens3"] - - assert primary_cfg["dhcp4"] is False - assert primary_cfg["dhcp6"] is True - assert "02:00:17:0d:6b:be" == primary_cfg["match"]["macaddress"] - assert 9000 == primary_cfg["mtu"] - assert "addresses" not in primary_cfg - - assert "ens4" in nic_cfg - secondary_cfg = nic_cfg["ens4"] - assert secondary_cfg["dhcp4"] is False - assert secondary_cfg["dhcp6"] is False - assert "02:00:17:18:f6:ff" == secondary_cfg["match"]["macaddress"] - assert 9000 == secondary_cfg["mtu"] - - assert 1 == len(secondary_cfg["addresses"]) - assert ( - "2603:c020:400d:5d7e:aacc:8e5f:3b1b:3a4a/128" - == secondary_cfg["addresses"][0] - ) - @pytest.mark.parametrize("error_add_network", [None, Exception]) @pytest.mark.parametrize( "configure_secondary_nics", From 08a9dcf34445be572848f26171f1e343145d2564 Mon Sep 17 00:00:00 2001 From: Amy Chen <66719270+xiachen-rh@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:24:23 +0800 Subject: [PATCH 044/150] feat(azure): Implement the clean callback for DataSourceAzure (#6321) When the user runs 'cloud-init clean -c datasource' or 'cloudinit clean -c all' command for Azure DataSouce, it would remove the azure resource disk line in /etc/fstab. Signed-off-by: Amy Chen --- cloudinit/sources/DataSourceAzure.py | 37 +++++++++++++++++++++++++++ tests/unittests/sources/test_azure.py | 28 ++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 7829fa535dd..d340a0996c6 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -20,6 +20,7 @@ import requests from cloudinit import net, performance, sources, ssh_util, subp, util +from cloudinit.config import cc_mounts from cloudinit.event import EventScope, EventType from cloudinit.net import device_driver from cloudinit.net.dhcp import ( @@ -1669,6 +1670,42 @@ def validate_imds_network_metadata(self, imds_md: dict) -> bool: return False + def _cleanup_resourcedisk_fstab(self): + """ + Remove resource disk entries from fstab, which are configured + by cloud-init i.e. lines containing "/dev/disk/cloud/azure_resource" + and cloudconfig comment. + """ + fstab_path = cc_mounts.FSTAB_PATH + mnt_comment = cc_mounts.MNT_COMMENT + removed_entries = RESOURCE_DISK_PATH + if not os.path.exists(fstab_path): + return + + with open(fstab_path, "r") as f: + lines = f.readlines() + new_lines = [] + changed = False + for line in lines: + if removed_entries in line and mnt_comment in line: + changed = True + continue + new_lines.append(line) + # rewrite fstab + try: + if changed: + with open(fstab_path, "w") as f: + f.writelines(new_lines) + LOG.info("Removed resource disk entries from %s", fstab_path) + except Exception as e: + LOG.warning( + "Failed to clean resource disk entries from fstab: %s", e + ) + + def clean(self): + # Azure-specific cleanup logic for "cloud-init clean -c datasource" + self._cleanup_resourcedisk_fstab() + def _username_from_imds(imds_data): try: diff --git a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py index fbc90a2d853..d7f0adb4ab4 100644 --- a/tests/unittests/sources/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -16,6 +16,7 @@ from cloudinit import distros, dmi, helpers, subp, url_helper from cloudinit.atomic_helper import b64e, json_dumps +from cloudinit.config import cc_mounts from cloudinit.net import dhcp, ephemeral from cloudinit.sources import UNSET from cloudinit.sources import DataSourceAzure as dsaz @@ -2352,6 +2353,33 @@ def test_userdata_from_imds_with_customdata_from_OVF(self, get_ds): assert ret assert dsrc.userdata_raw == userdataOVF.encode("utf-8") + @pytest.mark.usefixtures("fake_filesystem") + def test_cleanup_resourcedisk_fstab(self, get_ds): + """Ensure that cloud-init clean will remove resource disk entries + from /etc/fstab""" + fstab_original_content = ( + "UUID=abc123 / ext4 defaults 0 0\n" + "/dev/disk/cloud/azure_resource-part1 /mnt " + "auto defaults,nofail,x-systemd.after=" + "cloud-init.service,_netdev,comment=cloudconfig 0 2\n" + ) + fstab_expected_content = "UUID=abc123 / ext4 defaults 0 0\n" + + etc_path = "/etc" + if not os.path.exists(etc_path): + os.makedirs(etc_path) + fstab_path = cc_mounts.FSTAB_PATH + with open(fstab_path, "w") as fd: + fd.write(fstab_original_content) + + data = {} + dsrc = get_ds(data) + dsrc.clean() + + with open(fstab_path, "r") as fd: + fstab_new_content = fd.read() + assert fstab_expected_content == fstab_new_content + class TestLoadAzureDsDir: """Tests for load_azure_ds_dir.""" From b92f3cd5044e4a12bafc747d731c29d609a344bd Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Fri, 25 Jul 2025 13:50:46 +0200 Subject: [PATCH 045/150] chore: add typing to cloudinit.net.eni GH-5445 Signed-off-by: Alberto Contreras --- cloudinit/net/eni.py | 44 +++++++++++++++++++++++++++----------------- pyproject.toml | 1 - 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 17245fa3271..20d313572ac 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -6,7 +6,7 @@ import os import re from contextlib import suppress -from typing import Optional +from typing import Any, Dict, List, Optional from cloudinit import performance, subp, util from cloudinit.net import ParserError, renderer, subnet_is_ipv6 @@ -62,7 +62,7 @@ # TODO: switch valid_map based on mode inet/inet6 -def _iface_add_subnet(iface, subnet): +def _iface_add_subnet(iface: dict, subnet: dict) -> List[str]: content = [] valid_map = [ "address", @@ -92,7 +92,9 @@ def _iface_add_subnet(iface, subnet): # TODO: switch to valid_map for attrs -def _iface_add_attrs(iface, index, ipv4_subnet_mtu): +def _iface_add_attrs( + iface: dict, index: int, ipv4_subnet_mtu: Optional[str] +) -> List[str]: # If the index is non-zero, this is an alias interface. Alias interfaces # represent additional interface addresses, and should not have additional # attributes. (extra attributes here are almost always either incorrect, @@ -153,7 +155,9 @@ def _iface_add_attrs(iface, index, ipv4_subnet_mtu): return sorted(content) -def _iface_start_entry(iface, index, render_hwaddress=False): +def _iface_start_entry( + iface: dict, index, render_hwaddress: bool = False +) -> List[str]: fullname = iface["name"] control = iface["control"] @@ -177,7 +181,9 @@ def _iface_start_entry(iface, index, render_hwaddress=False): return lines -def _parse_deb_config_data(ifaces, contents, src_dir, src_path): +def _parse_deb_config_data( + ifaces: dict, contents: str, src_dir: str, src_path: str +) -> None: """Parses the file contents, placing result into ifaces. '_source_path' is added to every dictionary entry to define which file @@ -310,14 +316,14 @@ def _parse_deb_config_data(ifaces, contents, src_dir, src_path): @performance.timed("Converting eni data") -def convert_eni_data(eni_data): +def convert_eni_data(eni_data: str) -> dict: """Return a network config representation of what is in eni_data""" - ifaces = {} - _parse_deb_config_data(ifaces, eni_data, src_dir=None, src_path=None) + ifaces: dict = {} + _parse_deb_config_data(ifaces, eni_data, src_dir="None", src_path="None") return _ifaces_to_net_config_data(ifaces) -def _ifaces_to_net_config_data(ifaces): +def _ifaces_to_net_config_data(ifaces: dict) -> dict: """Return network config that represents the ifaces data provided. ifaces = _parse_deb_config_data(...) config = ifaces_to_net_config_data(ifaces) @@ -361,7 +367,7 @@ def _ifaces_to_net_config_data(ifaces): class Renderer(renderer.Renderer): """Renders network information in a /etc/network/interfaces format.""" - def __init__(self, config=None): + def __init__(self, config: Optional[dict] = None): if not config: config = {} self.eni_path = config.get("eni_path", "etc/network/interfaces") @@ -370,7 +376,7 @@ def __init__(self, config=None): "netrules_path", "etc/udev/rules.d/70-persistent-net.rules" ) - def _render_route(self, route, indent=""): + def _render_route(self, route: dict, indent: str = "") -> List[str]: """When rendering routes for an iface, in some cases applying a route may result in the route command returning non-zero which produces some confusing output for users manually using ifup/ifdown[1]. To @@ -422,13 +428,15 @@ def _render_route(self, route, indent=""): content.append(down + route_line + or_true) return content - def _render_iface(self, iface, render_hwaddress=False): + def _render_iface( + self, iface: dict, render_hwaddress: bool = False + ) -> List[List[str]]: iface = copy.deepcopy(iface) # Remove irrelevant keys with suppress(KeyError): iface.pop("config_id") - sections = [] + sections: List[List[str]] = [] subnets = iface.get("subnets", {}) accept_ra = iface.pop("accept-ra", None) ethernet_wol = iface.pop("wakeonlan", None) @@ -504,12 +512,14 @@ def _render_iface(self, iface, render_hwaddress=False): sections.append(lines) return sections - def _render_interfaces(self, network_state, render_hwaddress=False): + def _render_interfaces( + self, network_state: NetworkState, render_hwaddress: bool = False + ) -> str: """Given state, emit etc/network/interfaces content.""" # handle 'lo' specifically as we need to insert the global dns entries # there (as that is the only interface that will be always up). - lo = { + lo: Dict[str, Any] = { "name": "lo", "type": "physical", "inet": "inet", @@ -560,7 +570,7 @@ def render_network_state( self, network_state: NetworkState, templates: Optional[dict] = None, - target=None, + target: Optional[str] = None, ) -> None: fpeni = subp.target_path(target, self.eni_path) util.ensure_dir(os.path.dirname(fpeni)) @@ -577,7 +587,7 @@ def render_network_state( ) -def available(target=None): +def available(target: Optional[str] = None) -> bool: expected = ["ifquery", "ifup", "ifdown"] search = ["/sbin", "/usr/sbin"] for p in expected: diff --git a/pyproject.toml b/pyproject.toml index 8593894e95e..0d473ba9020 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,6 @@ module = [ "cloudinit.distros.ug_util", "cloudinit.helpers", "cloudinit.net.cmdline", - "cloudinit.net.eni", "cloudinit.net.ephemeral", "cloudinit.net.freebsd", "cloudinit.net.netbsd", From a729cd7e518a1ef27fc005c15178c603282d8af4 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 30 Jul 2025 12:22:47 +0200 Subject: [PATCH 046/150] chore: remove unused target arg from Activator.available --- cloudinit/net/activators.py | 37 ++++++++++++-------------- cloudinit/net/eni.py | 6 ++--- cloudinit/net/netplan.py | 4 +-- cloudinit/net/network_manager.py | 4 +-- cloudinit/net/networkd.py | 2 +- cloudinit/net/renderers.py | 15 +++++------ tests/unittests/test_net_activators.py | 29 ++++++++------------ 7 files changed, 42 insertions(+), 55 deletions(-) diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py index 9421289413a..5200c428e8c 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -2,7 +2,7 @@ import logging from abc import ABC, abstractmethod from functools import partial -from typing import Callable, Dict, Iterable, List, Optional, Type, Union +from typing import Callable, Dict, Iterable, List, Optional, Type from cloudinit import subp, util from cloudinit.net import eni, netplan, network_manager, networkd @@ -45,7 +45,7 @@ def _alter_interface_callable( class NetworkActivator(ABC): @staticmethod @abstractmethod - def available(target: Optional[str] = None) -> bool: + def available() -> bool: """Return True if activator is available, otherwise return False.""" raise NotImplementedError() @@ -97,9 +97,9 @@ class IfUpDownActivator(NetworkActivator): # E.g., NetworkManager has a ifupdown plugin that requires the name # of a specific connection. @staticmethod - def available(target: Optional[str] = None) -> bool: + def available() -> bool: """Return true if ifupdown can be used on this system.""" - return eni.available(target=target) + return eni.available() @staticmethod def bring_up_interface(device_name: str) -> bool: @@ -122,11 +122,11 @@ def bring_down_interface(device_name: str) -> bool: class IfConfigActivator(NetworkActivator): @staticmethod - def available(target=None) -> bool: + def available() -> bool: """Return true if ifconfig can be used on this system.""" expected = "ifconfig" search = ["/sbin"] - return bool(subp.which(expected, search=search, target=target)) + return bool(subp.which(expected, search=search)) @staticmethod def bring_up_interface(device_name: str) -> bool: @@ -149,9 +149,9 @@ def bring_down_interface(device_name: str) -> bool: class NetworkManagerActivator(NetworkActivator): @staticmethod - def available(target=None) -> bool: + def available() -> bool: """Return true if NetworkManager can be used on this system.""" - return network_manager.available(target=target) + return network_manager.available() @staticmethod def bring_up_interface(device_name: str) -> bool: @@ -215,9 +215,9 @@ class NetplanActivator(NetworkActivator): NETPLAN_CMD = ["netplan", "apply"] @staticmethod - def available(target=None) -> bool: + def available() -> bool: """Return true if netplan can be used on this system.""" - return netplan.available(target=target) + return netplan.available() @staticmethod def bring_up_interface(device_name: str) -> bool: @@ -283,9 +283,9 @@ def wait_for_network() -> None: class NetworkdActivator(NetworkActivator): @staticmethod - def available(target=None) -> bool: + def available() -> bool: """Return true if ifupdown can be used on this system.""" - return networkd.available(target=target) + return networkd.available() @staticmethod def bring_up_interface(device_name: str) -> bool: @@ -334,9 +334,7 @@ def wait_for_network() -> None: } -def search_activator( - priority: List[str], target: Union[str, None] -) -> Optional[Type[NetworkActivator]]: +def search_activator(priority: List[str]) -> Optional[Type[NetworkActivator]]: """Returns the first available activator from the priority list or None.""" unknown = [i for i in priority if i not in DEFAULT_PRIORITY] if unknown: @@ -348,22 +346,21 @@ def search_activator( ( activator_cls for activator_cls in activator_classes - if activator_cls.available(target) + if activator_cls.available() ), None, ) def select_activator( - priority: Optional[List[str]] = None, target: Optional[str] = None + priority: Optional[List[str]] = None, ) -> Type[NetworkActivator]: if priority is None: priority = DEFAULT_PRIORITY - selected = search_activator(priority, target) + selected = search_activator(priority) if not selected: - tmsg = f" in target={target}" if target and target != "/" else "" raise NoActivatorException( - f"No available network activators found{tmsg}. " + f"No available network activators found. " f"Searched through list: {priority}" ) LOG.debug( diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 20d313572ac..43ace88777d 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -587,13 +587,13 @@ def render_network_state( ) -def available(target: Optional[str] = None) -> bool: +def available() -> bool: expected = ["ifquery", "ifup", "ifdown"] search = ["/sbin", "/usr/sbin"] for p in expected: - if not subp.which(p, search=search, target=target): + if not subp.which(p, search=search): return False - eni = subp.target_path(target, "etc/network/interfaces") + eni = "/etc/network/interfaces" if not os.path.isfile(eni): return False diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 64a3c4786a3..ae90db4ba0b 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -609,10 +609,10 @@ def _render_section(name, section): return "".join(content) -def available(target=None): +def available(): expected = ["netplan"] search = ["/usr/sbin", "/sbin"] for p in expected: - if not subp.which(p, search=search, target=target): + if not subp.which(p, search=search): return False return True diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py index cc907ce261b..d4c705f2726 100644 --- a/cloudinit/net/network_manager.py +++ b/cloudinit/net/network_manager.py @@ -646,12 +646,12 @@ def cloud_init_nm_conf_filename(target=None): return f"{target_con_dir}/conf.d/{conf_file}" -def available(target=None): +def available(): # TODO: Move `uses_systemd` to a more appropriate location # It is imported here to avoid circular import from cloudinit.distros import uses_systemd - nmcli_present = subp.which("nmcli", target=target) + nmcli_present = subp.which("nmcli") service_active = True if uses_systemd(): try: diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py index 345935d68a4..64c1925296a 100644 --- a/cloudinit/net/networkd.py +++ b/cloudinit/net/networkd.py @@ -363,6 +363,6 @@ def available(target=None): expected = ["ip", "systemctl"] search = ["/usr/sbin", "/bin"] for p in expected: - if not subp.which(p, search=search, target=target): + if not subp.which(p, search=search): return False return True diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py index e201bfe4b62..ab7b7d8bf90 100644 --- a/cloudinit/net/renderers.py +++ b/cloudinit/net/renderers.py @@ -39,7 +39,7 @@ def search( - priority=None, target=None, first=False + priority=None, first=False ) -> List[Tuple[str, Type[renderer.Renderer]]]: if priority is None: priority = DEFAULT_PRIORITY @@ -55,7 +55,7 @@ def search( found = [] for name in priority: render_mod = available[name] - if render_mod.available(target): + if render_mod.available(): cur = (name, render_mod.Renderer) if first: return [cur] @@ -64,16 +64,13 @@ def search( return found -def select(priority=None, target=None) -> Tuple[str, Type[renderer.Renderer]]: - found = search(priority, target=target, first=True) +def select(priority=None) -> Tuple[str, Type[renderer.Renderer]]: + found = search(priority, first=True) if not found: if priority is None: priority = DEFAULT_PRIORITY - tmsg = "" - if target and target != "/": - tmsg = " in target=%s" % target raise RendererNotFoundError( - "No available network renderers found%s. Searched through list: %s" - % (tmsg, priority) + "No available network renderers found. Searched through list: %s" + % priority ) return found[0] diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py index 84876b73b41..4cbb6f1001b 100644 --- a/tests/unittests/test_net_activators.py +++ b/tests/unittests/test_net_activators.py @@ -83,7 +83,7 @@ def unavailable_mocks(): class TestSearchAndSelect: def test_empty_list(self, available_mocks): - resp = search_activator(priority=DEFAULT_PRIORITY, target=None) + resp = search_activator(priority=DEFAULT_PRIORITY) assert resp == NAME_TO_ACTIVATOR[DEFAULT_PRIORITY[0]] activator = select_activator() @@ -91,19 +91,12 @@ def test_empty_list(self, available_mocks): def test_priority(self, available_mocks): new_order = ["netplan", "network-manager"] - resp = search_activator(priority=new_order, target=None) + resp = search_activator(priority=new_order) assert resp == NetplanActivator activator = select_activator(priority=new_order) assert activator == NetplanActivator - def test_target(self, available_mocks): - search_activator(priority=DEFAULT_PRIORITY, target="/tmp") - assert "/tmp" == available_mocks.m_which.call_args[1]["target"] - - select_activator(target="/tmp") - assert "/tmp" == available_mocks.m_which.call_args[1]["target"] - @patch( "cloudinit.net.activators.IfUpDownActivator.available", return_value=False, @@ -111,7 +104,7 @@ def test_target(self, available_mocks): def test_first_not_available(self, m_available, available_mocks): # We've mocked out IfUpDownActivator as unavailable, so expect the # next in the list of default priorities - resp = search_activator(priority=DEFAULT_PRIORITY, target=None) + resp = search_activator(priority=DEFAULT_PRIORITY) assert resp == NAME_TO_ACTIVATOR[DEFAULT_PRIORITY[1]] resp = select_activator() @@ -119,12 +112,12 @@ def test_first_not_available(self, m_available, available_mocks): def test_priority_not_exist(self, available_mocks): with pytest.raises(ValueError): - search_activator(priority=["spam", "eggs"], target=None) + search_activator(priority=["spam", "eggs"]) with pytest.raises(ValueError): select_activator(priority=["spam", "eggs"]) def test_none_available(self, unavailable_mocks): - resp = search_activator(priority=DEFAULT_PRIORITY, target=None) + resp = search_activator(priority=DEFAULT_PRIORITY) assert resp is None with pytest.raises(NoActivatorException): @@ -132,18 +125,18 @@ def test_none_available(self, unavailable_mocks): IF_UP_DOWN_AVAILABLE_CALLS = [ - (("ifquery",), {"search": ["/sbin", "/usr/sbin"], "target": None}), - (("ifup",), {"search": ["/sbin", "/usr/sbin"], "target": None}), - (("ifdown",), {"search": ["/sbin", "/usr/sbin"], "target": None}), + (("ifquery",), {"search": ["/sbin", "/usr/sbin"]}), + (("ifup",), {"search": ["/sbin", "/usr/sbin"]}), + (("ifdown",), {"search": ["/sbin", "/usr/sbin"]}), ] NETPLAN_AVAILABLE_CALLS = [ - (("netplan",), {"search": ["/usr/sbin", "/sbin"], "target": None}), + (("netplan",), {"search": ["/usr/sbin", "/sbin"]}), ] NETWORKD_AVAILABLE_CALLS = [ - (("ip",), {"search": ["/usr/sbin", "/bin"], "target": None}), - (("systemctl",), {"search": ["/usr/sbin", "/bin"], "target": None}), + (("ip",), {"search": ["/usr/sbin", "/bin"]}), + (("systemctl",), {"search": ["/usr/sbin", "/bin"]}), ] From c3e881f2c4f161b42c0afcb6efee3fbbe9de472a Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 30 Jul 2025 19:24:15 +0200 Subject: [PATCH 047/150] test: drop citestcase from azure_helper tests (#6335) --- tests/unittests/sources/test_azure_helper.py | 620 ++++++++----------- 1 file changed, 263 insertions(+), 357 deletions(-) diff --git a/tests/unittests/sources/test_azure_helper.py b/tests/unittests/sources/test_azure_helper.py index a6b2b4e5b4e..4fc252b9ca7 100644 --- a/tests/unittests/sources/test_azure_helper.py +++ b/tests/unittests/sources/test_azure_helper.py @@ -4,8 +4,8 @@ import os import re import unittest -from contextlib import ExitStack from textwrap import dedent +from unittest import mock from xml.etree import ElementTree as ET from xml.sax.saxutils import escape, unescape @@ -18,7 +18,6 @@ from cloudinit.sources.helpers import azure as azure_helper from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim from cloudinit.util import load_text_file -from tests.unittests.helpers import CiTestCase, mock from tests.unittests.sources.test_azure import construct_ovf_env from tests.unittests.util import MockDistro @@ -159,7 +158,7 @@ def test_get_ip_from_lease_value(self, encoded_address, ip_address): ) -class TestGoalStateParsing(CiTestCase): +class TestGoalStateParsing: default_parameters = { "incarnation": 1, "container_id": "MyContainerId", @@ -189,17 +188,17 @@ def _get_goal_state(self, m_azure_endpoint_client=None, **kwargs): def test_incarnation_parsed_correctly(self): incarnation = "123" goal_state = self._get_goal_state(incarnation=incarnation) - self.assertEqual(incarnation, goal_state.incarnation) + assert incarnation == goal_state.incarnation def test_container_id_parsed_correctly(self): container_id = "TestContainerId" goal_state = self._get_goal_state(container_id=container_id) - self.assertEqual(container_id, goal_state.container_id) + assert container_id == goal_state.container_id def test_instance_id_parsed_correctly(self): instance_id = "TestInstanceId" goal_state = self._get_goal_state(instance_id=instance_id) - self.assertEqual(instance_id, goal_state.instance_id) + assert instance_id == goal_state.instance_id def test_certificates_xml_parsed_and_fetched_correctly(self): m_azure_endpoint_client = mock.MagicMock() @@ -209,15 +208,12 @@ def test_certificates_xml_parsed_and_fetched_correctly(self): certificates_url=certificates_url, ) certificates_xml = goal_state.certificates_xml - self.assertEqual(1, m_azure_endpoint_client.get.call_count) - self.assertEqual( - certificates_url, m_azure_endpoint_client.get.call_args[0][0] - ) - self.assertTrue( - m_azure_endpoint_client.get.call_args[1].get("secure", False) - ) - self.assertEqual( - m_azure_endpoint_client.get.return_value.contents, certificates_xml + assert 1 == m_azure_endpoint_client.get.call_count + assert certificates_url == m_azure_endpoint_client.get.call_args[0][0] + assert m_azure_endpoint_client.get.call_args[1].get("secure", False) + assert ( + m_azure_endpoint_client.get.return_value.contents + == certificates_xml ) def test_missing_certificates_skips_http_get(self): @@ -227,66 +223,60 @@ def test_missing_certificates_skips_http_get(self): certificates_url=None, ) certificates_xml = goal_state.certificates_xml - self.assertEqual(0, m_azure_endpoint_client.get.call_count) - self.assertIsNone(certificates_xml) + assert 0 == m_azure_endpoint_client.get.call_count + assert certificates_xml is None def test_invalid_goal_state_xml_raises_parse_error(self): xml = "random non-xml data" - with self.assertRaises(ET.ParseError): + with pytest.raises(ET.ParseError): azure_helper.GoalState(xml, mock.MagicMock()) def test_missing_container_id_in_goal_state_xml_raises_exc(self): xml = self._get_formatted_goal_state_xml_string() xml = re.sub(".*", "", xml) - with self.assertRaises(azure_helper.InvalidGoalStateXMLException): + with pytest.raises(azure_helper.InvalidGoalStateXMLException): azure_helper.GoalState(xml, mock.MagicMock()) def test_missing_instance_id_in_goal_state_xml_raises_exc(self): xml = self._get_formatted_goal_state_xml_string() xml = re.sub(".*", "", xml) - with self.assertRaises(azure_helper.InvalidGoalStateXMLException): + with pytest.raises(azure_helper.InvalidGoalStateXMLException): azure_helper.GoalState(xml, mock.MagicMock()) def test_missing_incarnation_in_goal_state_xml_raises_exc(self): xml = self._get_formatted_goal_state_xml_string() xml = re.sub(".*", "", xml) - with self.assertRaises(azure_helper.InvalidGoalStateXMLException): + with pytest.raises(azure_helper.InvalidGoalStateXMLException): azure_helper.GoalState(xml, mock.MagicMock()) -class TestAzureEndpointHttpClient(CiTestCase): +@mock.patch("cloudinit.sources.helpers.azure.http_with_retries") +class TestAzureEndpointHttpClient: regular_headers = { "x-ms-agent-name": "WALinuxAgent", "x-ms-version": "2012-11-30", } - def setUp(self): - super(TestAzureEndpointHttpClient, self).setUp() - patches = ExitStack() - self.addCleanup(patches.close) - self.m_http_with_retries = patches.enter_context( - mock.patch.object(azure_helper, "http_with_retries") - ) - - def test_non_secure_get(self): + def test_non_secure_get(self, m_http_with_retries): client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) url = "MyTestUrl" response = client.get(url, secure=False) - self.assertEqual(1, self.m_http_with_retries.call_count) - self.assertEqual(self.m_http_with_retries.return_value, response) - self.assertEqual( - mock.call(url, headers=self.regular_headers), - self.m_http_with_retries.call_args, + assert 1 == m_http_with_retries.call_count + assert m_http_with_retries.return_value == response + assert ( + mock.call(url, headers=self.regular_headers) + == m_http_with_retries.call_args ) - def test_non_secure_get_raises_exception(self): + def test_non_secure_get_raises_exception(self, m_http_with_retries): client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) url = "MyTestUrl" - self.m_http_with_retries.side_effect = SentinelException - self.assertRaises(SentinelException, client.get, url, secure=False) - self.assertEqual(1, self.m_http_with_retries.call_count) + m_http_with_retries.side_effect = SentinelException + with pytest.raises(SentinelException): + client.get(url, secure=False) + assert 1 == m_http_with_retries.call_count - def test_secure_get(self): + def test_secure_get(self, m_http_with_retries): url = "MyTestUrl" m_certificate = mock.MagicMock() expected_headers = self.regular_headers.copy() @@ -298,67 +288,70 @@ def test_secure_get(self): ) client = azure_helper.AzureEndpointHttpClient(m_certificate) response = client.get(url, secure=True) - self.assertEqual(1, self.m_http_with_retries.call_count) - self.assertEqual(self.m_http_with_retries.return_value, response) - self.assertEqual( - mock.call(url, headers=expected_headers), - self.m_http_with_retries.call_args, + assert 1 == m_http_with_retries.call_count + assert m_http_with_retries.return_value == response + assert ( + mock.call(url, headers=expected_headers) + == m_http_with_retries.call_args ) - def test_secure_get_raises_exception(self): + def test_secure_get_raises_exception(self, m_http_with_retries): url = "MyTestUrl" client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) - self.m_http_with_retries.side_effect = SentinelException - self.assertRaises(SentinelException, client.get, url, secure=True) - self.assertEqual(1, self.m_http_with_retries.call_count) + m_http_with_retries.side_effect = SentinelException + with pytest.raises(SentinelException): + client.get(url, secure=True) + assert 1 == m_http_with_retries.call_count - def test_post(self): + def test_post(self, m_http_with_retries): m_data = mock.MagicMock() url = "MyTestUrl" client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) response = client.post(url, data=m_data) - self.assertEqual(1, self.m_http_with_retries.call_count) - self.assertEqual(self.m_http_with_retries.return_value, response) - self.assertEqual( - mock.call(url, data=m_data, headers=self.regular_headers), - self.m_http_with_retries.call_args, + assert 1 == m_http_with_retries.call_count + assert m_http_with_retries.return_value == response + assert ( + mock.call(url, data=m_data, headers=self.regular_headers) + == m_http_with_retries.call_args ) - def test_post_raises_exception(self): + def test_post_raises_exception(self, m_http_with_retries): m_data = mock.MagicMock() url = "MyTestUrl" client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) - self.m_http_with_retries.side_effect = SentinelException - self.assertRaises(SentinelException, client.post, url, data=m_data) - self.assertEqual(1, self.m_http_with_retries.call_count) + m_http_with_retries.side_effect = SentinelException + with pytest.raises(SentinelException): + client.post(url, data=m_data) + assert 1 == m_http_with_retries.call_count - def test_post_with_extra_headers(self): + def test_post_with_extra_headers(self, m_http_with_retries): url = "MyTestUrl" client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) extra_headers = {"test": "header"} client.post(url, extra_headers=extra_headers) expected_headers = self.regular_headers.copy() expected_headers.update(extra_headers) - self.assertEqual(1, self.m_http_with_retries.call_count) - self.assertEqual( - mock.call(url, data=mock.ANY, headers=expected_headers), - self.m_http_with_retries.call_args, + assert 1 == m_http_with_retries.call_count + assert ( + mock.call(url, data=mock.ANY, headers=expected_headers) + == m_http_with_retries.call_args ) - def test_post_with_sleep_with_extra_headers_raises_exception(self): + def test_post_with_sleep_with_extra_headers_raises_exception( + self, m_http_with_retries + ): m_data = mock.MagicMock() url = "MyTestUrl" extra_headers = {"test": "header"} client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) - self.m_http_with_retries.side_effect = SentinelException - self.assertRaises( - SentinelException, - client.post, - url, - data=m_data, - extra_headers=extra_headers, - ) - self.assertEqual(1, self.m_http_with_retries.call_count) + m_http_with_retries.side_effect = SentinelException + with pytest.raises(SentinelException): + client.post( + url, + data=m_data, + extra_headers=extra_headers, + ) + assert 1 == m_http_with_retries.call_count class TestHttpWithRetries: @@ -484,52 +477,42 @@ def test_success( ) in caplog.record_tuples -class TestOpenSSLManager(CiTestCase): - def setUp(self): - super(TestOpenSSLManager, self).setUp() - patches = ExitStack() - self.addCleanup(patches.close) - - self.subp = patches.enter_context( - mock.patch.object(azure_helper.subp, "subp") - ) +@mock.patch("cloudinit.sources.helpers.azure.subp.subp") +class TestOpenSSLManager: + @pytest.fixture(autouse=True) + def fixtures(self, mocker): try: - self.open = patches.enter_context(mock.patch("__builtin__.open")) + mocker.patch("__builtin__.open") except ImportError: - self.open = patches.enter_context(mock.patch("builtins.open")) + mocker.patch("builtins.open") @mock.patch.object(azure_helper, "cd", mock.MagicMock()) @mock.patch.object(azure_helper.temp_utils, "mkdtemp") - def test_openssl_manager_creates_a_tmpdir(self, mkdtemp): + def test_openssl_manager_creates_a_tmpdir(self, mkdtemp, m_subp): manager = azure_helper.OpenSSLManager() - self.assertEqual(mkdtemp.return_value, manager.tmpdir) + assert mkdtemp.return_value == manager.tmpdir - def test_generate_certificate_uses_tmpdir(self): + def test_generate_certificate_uses_tmpdir(self, m_subp): subp_directory = {} def capture_directory(*args, **kwargs): subp_directory["path"] = os.getcwd() - self.subp.side_effect = capture_directory + m_subp.side_effect = capture_directory manager = azure_helper.OpenSSLManager() - self.assertEqual(manager.tmpdir, subp_directory["path"]) + assert manager.tmpdir == subp_directory["path"] manager.clean_up() @mock.patch.object(azure_helper, "cd", mock.MagicMock()) @mock.patch.object(azure_helper.temp_utils, "mkdtemp", mock.MagicMock()) @mock.patch.object(azure_helper.util, "del_dir") - def test_clean_up(self, del_dir): + def test_clean_up(self, del_dir, m_subp): manager = azure_helper.OpenSSLManager() manager.clean_up() - self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list) - - -class TestOpenSSLManagerActions(CiTestCase): - def setUp(self): - super(TestOpenSSLManagerActions, self).setUp() + assert [mock.call(manager.tmpdir)] == del_dir.call_args_list - self.allowed_subp = True +class TestOpenSSLManagerActions: def _data_file(self, name): path = "tests/data/azure" return os.path.join(path, name) @@ -540,11 +523,11 @@ def test_pubkey_extract(self): good_key = load_text_file(self._data_file("pubkey_extract_ssh_key")) sslmgr = azure_helper.OpenSSLManager() key = sslmgr._get_ssh_key_from_cert(cert) - self.assertEqual(good_key, key) + assert good_key == key good_fingerprint = "073E19D14D1C799224C6A0FD8DDAB6A8BF27D473" fingerprint = sslmgr._get_fingerprint_from_cert(cert) - self.assertEqual(good_fingerprint, fingerprint) + assert good_fingerprint == fingerprint @unittest.skip("todo move to cloud_test") @mock.patch.object(azure_helper.OpenSSLManager, "_decrypt_certs_from_xml") @@ -564,12 +547,12 @@ def test_parse_certificates(self, mock_decrypt_certs): sslmgr = azure_helper.OpenSSLManager() keys_by_fp = sslmgr.parse_certificates("") for fp in keys_by_fp.keys(): - self.assertIn(fp, fingerprints) + assert fp in fingerprints for fp in fingerprints: - self.assertIn(fp, keys_by_fp) + assert fp in keys_by_fp -class TestGoalStateHealthReporter(CiTestCase): +class TestGoalStateHealthReporter: maxDiff = None default_parameters = { @@ -591,25 +574,14 @@ class TestGoalStateHealthReporter(CiTestCase): "Test error message containing provisioning failure details" ) - def setUp(self): - super(TestGoalStateHealthReporter, self).setUp() - patches = ExitStack() - self.addCleanup(patches.close) - - patches.enter_context( - mock.patch.object(azure_helper, "sleep", mock.MagicMock()) - ) - self.read_file_or_url = patches.enter_context( - mock.patch.object(azure_helper.url_helper, "read_file_or_url") - ) - - self.post = patches.enter_context( - mock.patch.object(azure_helper.AzureEndpointHttpClient, "post") - ) - - self.GoalState = patches.enter_context( - mock.patch.object(azure_helper, "GoalState") + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + mocker.patch.object(azure_helper, "sleep", mock.MagicMock()) + mocker.patch.object(azure_helper.url_helper, "read_file_or_url") + self.post = mocker.patch.object( + azure_helper.AzureEndpointHttpClient, "post" ) + self.GoalState = mocker.patch.object(azure_helper, "GoalState") self.GoalState.return_value.container_id = self.default_parameters[ "container_id" ] @@ -668,14 +640,14 @@ def test_send_ready_signal_sends_post_request(self): ) reporter.send_ready_signal() - self.assertEqual(1, self.post.call_count) - self.assertEqual( + assert 1 == self.post.call_count + assert ( mock.call( self.test_health_report_url, data=m_build_report.return_value, extra_headers=self.test_default_headers, - ), - self.post.call_args, + ) + == self.post.call_args ) def test_send_failure_signal_sends_post_request(self): @@ -692,14 +664,14 @@ def test_send_failure_signal_sends_post_request(self): description=self.provisioning_failure_err_description ) - self.assertEqual(1, self.post.call_count) - self.assertEqual( + assert 1 == self.post.call_count + assert ( mock.call( self.test_health_report_url, data=m_build_report.return_value, extra_headers=self.test_default_headers, - ), - self.post.call_args, + ) + == self.post.call_args ) def test_build_report_for_ready_signal_health_document(self): @@ -716,51 +688,42 @@ def test_build_report_for_ready_signal_health_document(self): status=self.provisioning_success_status, ) - self.assertEqual(health_document, generated_health_document) + assert health_document == generated_health_document generated_xroot = ET.fromstring(generated_health_document) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, "./GoalStateIncarnation" - ), - str(self.default_parameters["incarnation"]), - ) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, "./Container/ContainerId" - ), - str(self.default_parameters["container_id"]), - ) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, "./Container/RoleInstanceList/Role/InstanceId" - ), - str(self.default_parameters["instance_id"]), - ) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, - "./Container/RoleInstanceList/Role/Health/State", - ), - escape(self.provisioning_success_status), - ) - self.assertIsNone( + assert self._text_from_xpath_in_xroot( + generated_xroot, "./GoalStateIncarnation" + ) == str(self.default_parameters["incarnation"]) + assert self._text_from_xpath_in_xroot( + generated_xroot, "./Container/ContainerId" + ) == str(self.default_parameters["container_id"]) + assert self._text_from_xpath_in_xroot( + generated_xroot, "./Container/RoleInstanceList/Role/InstanceId" + ) == str(self.default_parameters["instance_id"]) + assert self._text_from_xpath_in_xroot( + generated_xroot, + "./Container/RoleInstanceList/Role/Health/State", + ) == escape(self.provisioning_success_status) + assert ( self._text_from_xpath_in_xroot( generated_xroot, "./Container/RoleInstanceList/Role/Health/Details", ) + is None ) - self.assertIsNone( + assert ( self._text_from_xpath_in_xroot( generated_xroot, "./Container/RoleInstanceList/Role/Health/Details/SubStatus", ) + is None ) - self.assertIsNone( + assert ( self._text_from_xpath_in_xroot( generated_xroot, "./Container/RoleInstanceList/Role/Health/Details/Description", ) + is None ) def test_build_report_for_failure_signal_health_document(self): @@ -779,48 +742,36 @@ def test_build_report_for_failure_signal_health_document(self): description=self.provisioning_failure_err_description, ) - self.assertEqual(health_document, generated_health_document) + assert health_document == generated_health_document generated_xroot = ET.fromstring(generated_health_document) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, "./GoalStateIncarnation" - ), - str(self.default_parameters["incarnation"]), - ) - self.assertEqual( + assert self._text_from_xpath_in_xroot( + generated_xroot, "./GoalStateIncarnation" + ) == str(self.default_parameters["incarnation"]) + assert ( self._text_from_xpath_in_xroot( generated_xroot, "./Container/ContainerId" - ), - self.default_parameters["container_id"], + ) + == self.default_parameters["container_id"] ) - self.assertEqual( + assert ( self._text_from_xpath_in_xroot( generated_xroot, "./Container/RoleInstanceList/Role/InstanceId" - ), - self.default_parameters["instance_id"], - ) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, - "./Container/RoleInstanceList/Role/Health/State", - ), - escape(self.provisioning_not_ready_status), - ) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, - "./Container/RoleInstanceList/Role/Health/Details/SubStatus", - ), - escape(self.provisioning_failure_substatus), - ) - self.assertEqual( - self._text_from_xpath_in_xroot( - generated_xroot, - "./Container/RoleInstanceList/Role/Health/Details/Description", - ), - escape(self.provisioning_failure_err_description), + ) + == self.default_parameters["instance_id"] ) + assert self._text_from_xpath_in_xroot( + generated_xroot, + "./Container/RoleInstanceList/Role/Health/State", + ) == escape(self.provisioning_not_ready_status) + assert self._text_from_xpath_in_xroot( + generated_xroot, + "./Container/RoleInstanceList/Role/Health/Details/SubStatus", + ) == escape(self.provisioning_failure_substatus) + assert self._text_from_xpath_in_xroot( + generated_xroot, + "./Container/RoleInstanceList/Role/Health/Details/Description", + ) == escape(self.provisioning_failure_err_description) def test_send_ready_signal_calls_build_report(self): with mock.patch.object( @@ -833,15 +784,15 @@ def test_send_ready_signal_calls_build_report(self): ) reporter.send_ready_signal() - self.assertEqual(1, m_build_report.call_count) - self.assertEqual( + assert 1 == m_build_report.call_count + assert ( mock.call( incarnation=self.default_parameters["incarnation"], container_id=self.default_parameters["container_id"], instance_id=self.default_parameters["instance_id"], status=self.provisioning_success_status, - ), - m_build_report.call_args, + ) + == m_build_report.call_args ) def test_send_failure_signal_calls_build_report(self): @@ -857,8 +808,8 @@ def test_send_failure_signal_calls_build_report(self): description=self.provisioning_failure_err_description ) - self.assertEqual(1, m_build_report.call_count) - self.assertEqual( + assert 1 == m_build_report.call_count + assert ( mock.call( incarnation=self.default_parameters["incarnation"], container_id=self.default_parameters["container_id"], @@ -866,8 +817,8 @@ def test_send_failure_signal_calls_build_report(self): status=self.provisioning_not_ready_status, substatus=self.provisioning_failure_substatus, description=self.provisioning_failure_err_description, - ), - m_build_report.call_args, + ) + == m_build_report.call_args ) def test_build_report_escapes_chars(self): @@ -906,7 +857,7 @@ def test_build_report_escapes_chars(self): description=health_description, ) - self.assertEqual(health_document, generated_health_document) + assert health_document == generated_health_document def test_build_report_conforms_to_length_limits(self): reporter = azure_helper.GoalStateHealthReporter( @@ -929,9 +880,9 @@ def test_build_report_conforms_to_length_limits(self): generated_xroot, "./Container/RoleInstanceList/Role/Health/Details/Description", ) - self.assertEqual( - len(unescape(generated_health_report_description)), - HEALTH_REPORT_DESCRIPTION_TRIM_LEN, + assert ( + len(unescape(generated_health_report_description)) + == HEALTH_REPORT_DESCRIPTION_TRIM_LEN ) def test_trim_description_then_escape_conforms_to_len_limits_worst_case( @@ -984,27 +935,20 @@ def test_trim_description_then_escape_conforms_to_len_limits_worst_case( ) # The escaped description string should be less than # the Azure platform limit for the escaped description string. - self.assertLessEqual(len(generated_health_report_description), 4096) - + assert len(generated_health_report_description) <= 4096 -class TestWALinuxAgentShim(CiTestCase): - def setUp(self): - super(TestWALinuxAgentShim, self).setUp() - patches = ExitStack() - self.addCleanup(patches.close) - self.AzureEndpointHttpClient = patches.enter_context( - mock.patch.object(azure_helper, "AzureEndpointHttpClient") - ) - self.GoalState = patches.enter_context( - mock.patch.object(azure_helper, "GoalState") - ) - self.OpenSSLManager = patches.enter_context( - mock.patch.object(azure_helper, "OpenSSLManager", autospec=True) +class TestWALinuxAgentShim: + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + self.AzureEndpointHttpClient = mocker.patch.object( + azure_helper, "AzureEndpointHttpClient" ) - patches.enter_context( - mock.patch.object(azure_helper, "sleep", mock.MagicMock()) + self.GoalState = mocker.patch.object(azure_helper, "GoalState") + self.OpenSSLManager = mocker.patch.object( + azure_helper, "OpenSSLManager", autospec=True ) + mocker.patch.object(azure_helper, "sleep", mock.MagicMock()) self.test_incarnation = "TestIncarnation" self.test_container_id = "TestContainerId" @@ -1027,54 +971,42 @@ def test_eject_iso_is_called(self): def test_http_client_does_not_use_certificate_for_report_ready(self): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_fetch_data(distro=None) - self.assertEqual( - [mock.call(None)], self.AzureEndpointHttpClient.call_args_list - ) + assert [mock.call(None)] == self.AzureEndpointHttpClient.call_args_list def test_http_client_does_not_use_certificate_for_report_failure(self): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_report_failure(description="TestDesc") - self.assertEqual( - [mock.call(None)], self.AzureEndpointHttpClient.call_args_list - ) + assert [mock.call(None)] == self.AzureEndpointHttpClient.call_args_list def test_correct_url_used_for_goalstate_during_report_ready(self): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_fetch_data(distro=None) m_get = self.AzureEndpointHttpClient.return_value.get - self.assertEqual( - [mock.call("http://test_endpoint/machine/?comp=goalstate")], - m_get.call_args_list, - ) - self.assertEqual( - [ - mock.call( - m_get.return_value.contents, - self.AzureEndpointHttpClient.return_value, - False, - ) - ], - self.GoalState.call_args_list, - ) + assert [ + mock.call("http://test_endpoint/machine/?comp=goalstate") + ] == m_get.call_args_list + assert [ + mock.call( + m_get.return_value.contents, + self.AzureEndpointHttpClient.return_value, + False, + ) + ] == self.GoalState.call_args_list def test_correct_url_used_for_goalstate_during_report_failure(self): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_report_failure(description="TestDesc") m_get = self.AzureEndpointHttpClient.return_value.get - self.assertEqual( - [mock.call("http://test_endpoint/machine/?comp=goalstate")], - m_get.call_args_list, - ) - self.assertEqual( - [ - mock.call( - m_get.return_value.contents, - self.AzureEndpointHttpClient.return_value, - False, - ) - ], - self.GoalState.call_args_list, - ) + assert [ + mock.call("http://test_endpoint/machine/?comp=goalstate") + ] == m_get.call_args_list + assert [ + mock.call( + m_get.return_value.contents, + self.AzureEndpointHttpClient.return_value, + False, + ) + ] == self.GoalState.call_args_list def test_certificates_used_to_determine_public_keys(self): # if register_with_azure_and_fetch_data() isn't passed some info about @@ -1095,13 +1027,12 @@ def test_certificates_used_to_determine_public_keys(self): data = shim.register_with_azure_and_fetch_data( distro=None, pubkey_info=mypk ) - self.assertEqual( - [mock.call(self.GoalState.return_value.certificates_xml)], - sslmgr.parse_certificates.call_args_list, - ) - self.assertIn("expected-key", data) - self.assertIn("expected-no-value-key", data) - self.assertNotIn("should-not-be-found", data) + assert [ + mock.call(self.GoalState.return_value.certificates_xml) + ] == sslmgr.parse_certificates.call_args_list + assert "expected-key" in data + assert "expected-no-value-key" in data + assert "should-not-be-found" not in data def test_absent_certificates_produces_empty_public_keys(self): mypk = [{"fingerprint": "fp1", "path": "path1"}] @@ -1110,25 +1041,23 @@ def test_absent_certificates_produces_empty_public_keys(self): data = shim.register_with_azure_and_fetch_data( distro=None, pubkey_info=mypk ) - self.assertEqual([], data) + assert [] == data def test_correct_url_used_for_report_ready(self): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_fetch_data(distro=None) expected_url = "http://test_endpoint/machine?comp=health" - self.assertEqual( - [mock.call(expected_url, data=mock.ANY, extra_headers=mock.ANY)], - self.AzureEndpointHttpClient.return_value.post.call_args_list, - ) + assert [ + mock.call(expected_url, data=mock.ANY, extra_headers=mock.ANY) + ] == self.AzureEndpointHttpClient.return_value.post.call_args_list def test_correct_url_used_for_report_failure(self): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_report_failure(description="TestDesc") expected_url = "http://test_endpoint/machine?comp=health" - self.assertEqual( - [mock.call(expected_url, data=mock.ANY, extra_headers=mock.ANY)], - self.AzureEndpointHttpClient.return_value.post.call_args_list, - ) + assert [ + mock.call(expected_url, data=mock.ANY, extra_headers=mock.ANY) + ] == self.AzureEndpointHttpClient.return_value.post.call_args_list def test_goal_state_values_used_for_report_ready(self): shim = wa_shim(endpoint="test_endpoint") @@ -1136,9 +1065,9 @@ def test_goal_state_values_used_for_report_ready(self): posted_document = ( self.AzureEndpointHttpClient.return_value.post.call_args[1]["data"] ) - self.assertIn(self.test_incarnation.encode("utf-8"), posted_document) - self.assertIn(self.test_container_id.encode("utf-8"), posted_document) - self.assertIn(self.test_instance_id.encode("utf-8"), posted_document) + assert self.test_incarnation.encode("utf-8") in posted_document + assert self.test_container_id.encode("utf-8") in posted_document + assert self.test_instance_id.encode("utf-8") in posted_document def test_goal_state_values_used_for_report_failure(self): shim = wa_shim(endpoint="test_endpoint") @@ -1146,9 +1075,9 @@ def test_goal_state_values_used_for_report_failure(self): posted_document = ( self.AzureEndpointHttpClient.return_value.post.call_args[1]["data"] ) - self.assertIn(self.test_incarnation.encode("utf-8"), posted_document) - self.assertIn(self.test_container_id.encode("utf-8"), posted_document) - self.assertIn(self.test_instance_id.encode("utf-8"), posted_document) + assert self.test_incarnation.encode("utf-8") in posted_document + assert self.test_container_id.encode("utf-8") in posted_document + assert self.test_instance_id.encode("utf-8") in posted_document def test_xml_elems_in_report_ready_post(self): shim = wa_shim(endpoint="test_endpoint") @@ -1163,7 +1092,7 @@ def test_xml_elems_in_report_ready_post(self): posted_document = ( self.AzureEndpointHttpClient.return_value.post.call_args[1]["data"] ) - self.assertEqual(health_document, posted_document) + assert health_document == posted_document def test_xml_elems_in_report_failure_post(self): shim = wa_shim(endpoint="test_endpoint") @@ -1183,7 +1112,7 @@ def test_xml_elems_in_report_failure_post(self): posted_document = ( self.AzureEndpointHttpClient.return_value.post.call_args[1]["data"] ) - self.assertEqual(health_document, posted_document) + assert health_document == posted_document @mock.patch.object(azure_helper, "GoalStateHealthReporter", autospec=True) def test_register_with_azure_and_fetch_data_calls_send_ready_signal( @@ -1191,9 +1120,9 @@ def test_register_with_azure_and_fetch_data_calls_send_ready_signal( ): shim = wa_shim(endpoint="test_endpoint") shim.register_with_azure_and_fetch_data(distro=None) - self.assertEqual( - 1, - m_goal_state_health_reporter.return_value.send_ready_signal.call_count, # noqa: E501 + assert ( + 1 + == m_goal_state_health_reporter.return_value.send_ready_signal.call_count # noqa: E501 ) @mock.patch.object(azure_helper, "GoalStateHealthReporter", autospec=True) @@ -1246,96 +1175,84 @@ def test_fetch_goalstate_during_report_ready_raises_exc_on_get_exc(self): url_helper.UrlError("retry", code=404) ) shim = wa_shim(endpoint="test_endpoint") - self.assertRaises( - url_helper.UrlError, shim.register_with_azure_and_fetch_data, None - ) + with pytest.raises(url_helper.UrlError): + shim.register_with_azure_and_fetch_data(None) def test_fetch_goalstate_during_report_failure_raises_exc_on_get_exc(self): self.AzureEndpointHttpClient.return_value.get.side_effect = ( url_helper.UrlError("retry", code=404) ) shim = wa_shim(endpoint="test_endpoint") - self.assertRaises( - url_helper.UrlError, - shim.register_with_azure_and_report_failure, - description="TestDesc", - ) + with pytest.raises(url_helper.UrlError): + shim.register_with_azure_and_report_failure( + description="TestDesc", + ) def test_fetch_goalstate_during_report_ready_raises_exc_on_parse_exc(self): self.GoalState.side_effect = url_helper.UrlError("retry", code=404) shim = wa_shim(endpoint="test_endpoint") - self.assertRaises( - url_helper.UrlError, shim.register_with_azure_and_fetch_data, None - ) + with pytest.raises(url_helper.UrlError): + shim.register_with_azure_and_fetch_data(None) def test_fetch_goalstate_during_report_failure_raises_exc_on_parse_exc( self, ): self.GoalState.side_effect = url_helper.UrlError("retry", code=404) shim = wa_shim(endpoint="test_endpoint") - self.assertRaises( - url_helper.UrlError, - shim.register_with_azure_and_report_failure, - description="TestDesc", - ) + with pytest.raises(url_helper.UrlError): + shim.register_with_azure_and_report_failure( + description="TestDesc", + ) def test_failure_to_send_report_ready_health_doc_bubbles_up(self): self.AzureEndpointHttpClient.return_value.post.side_effect = ( url_helper.UrlError("retry", code=404) ) shim = wa_shim(endpoint="test_endpoint") - self.assertRaises( - url_helper.UrlError, shim.register_with_azure_and_fetch_data, None - ) + with pytest.raises(url_helper.UrlError): + shim.register_with_azure_and_fetch_data(None) def test_failure_to_send_report_failure_health_doc_bubbles_up(self): self.AzureEndpointHttpClient.return_value.post.side_effect = ( url_helper.UrlError("retry", code=404) ) shim = wa_shim(endpoint="test_endpoint") - self.assertRaises( - url_helper.UrlError, - shim.register_with_azure_and_report_failure, - description="TestDesc", - ) - + with pytest.raises(url_helper.UrlError): + shim.register_with_azure_and_report_failure( + description="TestDesc", + ) -class TestGetMetadataGoalStateXMLAndReportReadyToFabric(CiTestCase): - def setUp(self): - super(TestGetMetadataGoalStateXMLAndReportReadyToFabric, self).setUp() - patches = ExitStack() - self.addCleanup(patches.close) - self.m_shim = patches.enter_context( - mock.patch.object(azure_helper, "WALinuxAgentShim") - ) +class TestGetMetadataGoalStateXMLAndReportReadyToFabric: + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + self.m_shim = mocker.patch.object(azure_helper, "WALinuxAgentShim") def test_data_from_shim_returned(self): ret = azure_helper.get_metadata_from_fabric( distro=None, endpoint="test_endpoint" ) - self.assertEqual( - self.m_shim.return_value.register_with_azure_and_fetch_data.return_value, # noqa: E501 - ret, + assert ( + self.m_shim.return_value.register_with_azure_and_fetch_data.return_value + == ret # noqa: E501 ) def test_success_calls_clean_up(self): azure_helper.get_metadata_from_fabric( distro=None, endpoint="test_endpoint" ) - self.assertEqual(1, self.m_shim.return_value.clean_up.call_count) + assert 1 == self.m_shim.return_value.clean_up.call_count def test_failure_in_registration_propagates_exc_and_calls_clean_up(self): self.m_shim.return_value.register_with_azure_and_fetch_data.side_effect = url_helper.UrlError( # noqa: E501 "retry", code=404 ) - self.assertRaises( - url_helper.UrlError, - azure_helper.get_metadata_from_fabric, - "test_endpoint", - None, - ) - self.assertEqual(1, self.m_shim.return_value.clean_up.call_count) + with pytest.raises(url_helper.UrlError): + azure_helper.get_metadata_from_fabric( + "test_endpoint", + None, + ) + assert 1 == self.m_shim.return_value.clean_up.call_count def test_calls_shim_register_with_azure_and_fetch_data(self): m_pubkey_info = mock.MagicMock() @@ -1345,45 +1262,35 @@ def test_calls_shim_register_with_azure_and_fetch_data(self): pubkey_info=m_pubkey_info, iso_dev="/dev/sr0", ) - self.assertEqual( - 1, - self.m_shim.return_value.register_with_azure_and_fetch_data.call_count, # noqa: E501 + assert ( + 1 + == self.m_shim.return_value.register_with_azure_and_fetch_data.call_count # noqa: E501 ) - self.assertEqual( + assert ( mock.call( distro=None, iso_dev="/dev/sr0", pubkey_info=m_pubkey_info - ), - self.m_shim.return_value.register_with_azure_and_fetch_data.call_args, # noqa: E501 + ) + == self.m_shim.return_value.register_with_azure_and_fetch_data.call_args # noqa: E501 ) def test_instantiates_shim_with_kwargs(self): azure_helper.get_metadata_from_fabric( endpoint="test_endpoint", distro=None ) - self.assertEqual(1, self.m_shim.call_count) - self.assertEqual( - mock.call(endpoint="test_endpoint"), - self.m_shim.call_args, - ) - + assert 1 == self.m_shim.call_count + assert mock.call(endpoint="test_endpoint") == self.m_shim.call_args -class TestGetMetadataGoalStateXMLAndReportFailureToFabric(CiTestCase): - def setUp(self): - super( - TestGetMetadataGoalStateXMLAndReportFailureToFabric, self - ).setUp() - patches = ExitStack() - self.addCleanup(patches.close) - self.m_shim = patches.enter_context( - mock.patch.object(azure_helper, "WALinuxAgentShim") - ) +class TestGetMetadataGoalStateXMLAndReportFailureToFabric: + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + self.m_shim = mocker.patch.object(azure_helper, "WALinuxAgentShim") def test_success_calls_clean_up(self): azure_helper.report_failure_to_fabric( endpoint="test_endpoint", encoded_report="test" ) - self.assertEqual(1, self.m_shim.return_value.clean_up.call_count) + assert 1 == self.m_shim.return_value.clean_up.call_count def test_failure_in_shim_report_failure_propagates_exc_and_calls_clean_up( self, @@ -1391,13 +1298,12 @@ def test_failure_in_shim_report_failure_propagates_exc_and_calls_clean_up( self.m_shim.return_value.register_with_azure_and_report_failure.side_effect = ( # noqa: E501 SentinelException ) - self.assertRaises( - SentinelException, - azure_helper.report_failure_to_fabric, - "test_endpoint", - encoded_report="test-report", - ) - self.assertEqual(1, self.m_shim.return_value.clean_up.call_count) + with pytest.raises(SentinelException): + azure_helper.report_failure_to_fabric( + "test_endpoint", + encoded_report="test-report", + ) + assert 1 == self.m_shim.return_value.clean_up.call_count def test_report_failure_to_fabric_calls_shim_report_failure( self, From 90d80fe3fe9a2a832b583823b3bba2f6d0a96a62 Mon Sep 17 00:00:00 2001 From: Ani Sinha Date: Fri, 1 Aug 2025 17:47:39 +0530 Subject: [PATCH 048/150] clean: do not attempt to find datasource when cache has been cleaned (#6325) When `cloud-init clean` is run with or without additional options but without `-s`, it removes everything except the seed directory from `/var/lib/cloud`. When `-s` is passed with or without other options, it removes the seed directory as well. A subsequent invocation of `cloud-init clean -c datasource` will fail to find the current datasource as the cache has been cleaned. Therefore, when cache is not present, do not try to find the datasource. Fixes: 34cb46e20f5d3f ("feat: add callback for datasources to clean config changes (#6100)") Signed-off-by: Ani Sinha --- cloudinit/cmd/clean.py | 27 ++++++++++++++---- tests/unittests/cmd/test_clean.py | 46 +++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/cloudinit/cmd/clean.py b/cloudinit/cmd/clean.py index fc25362ff59..94a9bfb8f9d 100755 --- a/cloudinit/cmd/clean.py +++ b/cloudinit/cmd/clean.py @@ -8,10 +8,11 @@ import argparse import glob +import logging import os import sys -from cloudinit import settings +from cloudinit import settings, sources from cloudinit.distros import uses_systemd from cloudinit.log import log_util from cloudinit.net.netplan import CLOUDINIT_NETPLAN_FILE @@ -25,6 +26,7 @@ write_file, ) +LOG = logging.getLogger(__name__) ETC_MACHINE_ID = "/etc/machine-id" GEN_NET_CONFIG_FILES = [ CLOUDINIT_NETPLAN_FILE, @@ -132,14 +134,29 @@ def remove_artifacts(init, remove_logs, remove_seed=False, remove_config=None): ): for conf in GEN_SSH_CONFIG_FILES: del_file(conf) - if remove_config and set(remove_config).intersection( - ["all", "datasource"] - ): - init.fetch().clean() + clean_datasource = remove_config and set(remove_config).intersection( + ["all", "datasource"] + ) if not os.path.isdir(init.paths.cloud_dir): + log_util.multi_log( + "Artifacts already cleaned.", + log=LOG, + log_level=logging.INFO, + ) return 0 # Artifacts dir already cleaned + + if clean_datasource: + try: + init.fetch().clean() + except sources.DataSourceNotFoundException: + log_util.multi_log( + "No datasource found, nothing cleaned.", + log=LOG, + log_level=logging.INFO, + ) + seed_path = os.path.join(init.paths.cloud_dir, "seed") for path in glob.glob("%s/*" % init.paths.cloud_dir): if path == seed_path and not remove_seed: diff --git a/tests/unittests/cmd/test_clean.py b/tests/unittests/cmd/test_clean.py index be39aba4ef1..5d6e4a19e8c 100644 --- a/tests/unittests/cmd/test_clean.py +++ b/tests/unittests/cmd/test_clean.py @@ -198,15 +198,57 @@ def test_keep_net_conf(self, clean_paths, init_class): assert conf_path.exists() is True, f"file {conf_path} removed!" assert 0 == retcode - def test_clean_datasource_conf(self, clean_paths, init_class): - """remove_config removes datasource files when True.""" + def test_clean_datasource_conf_without_cache( + self, clean_paths, init_class + ): + """remove_config does not remove datasource files when cache + is not present. + """ + ds_conf = clean_paths.tmpdir.join("/var/run/ds") + assert ds_conf.exists() is False, f"Unexpected {ds_conf}" + ensure_dir(os.path.dirname(ds_conf)) + ds_conf.write("#generated by generic DataSource\nfoobar\n") + assert ds_conf.exists() is True, "{ds_conf} not written!" + assert ( + clean_paths.cloud_dir.exists() is False + ), "unexpected cloud_dir present!" + + def ds_clean(): + print("deleting {ds_conf}") + del_file(ds_conf) + + ds = mock.Mock(spec=DataSource) + ds.clean = ds_clean + + def ds_fetch(): + return ds + + init_class.fetch = ds_fetch + retcode = clean.remove_artifacts( + init_class, + remove_logs=False, + remove_config=["datasource"], + ) + assert ds_conf.exists() is True, f"file {ds_conf} was removed!" + assert 0 == retcode + + def test_clean_datasource_conf_with_cache(self, clean_paths, init_class): + """remove_config removes datasource files when cache + is present. + """ + ensure_dir(clean_paths.cloud_dir) ds_conf = clean_paths.tmpdir.join("/var/run/ds") assert ds_conf.exists() is False, f"Unexpected {ds_conf}" ensure_dir(os.path.dirname(ds_conf)) ds_conf.write("#generated by generic DataSource\nfoobar\n") assert ds_conf.exists() is True, "{ds_conf} not written!" + cache = clean_paths.cloud_dir.join("instance") + assert cache.exists() is False, f"unexpected {cache} present!" + ensure_dir(cache) + assert cache.exists() is True, "{cache} not created!" + def ds_clean(): print("deleting {ds_conf}") del_file(ds_conf) From 12ce7d9fbde80f0507d4d88c572a358694319c79 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab <46541787+mostafaCamel@users.noreply.github.com> Date: Fri, 1 Aug 2025 23:38:52 +0300 Subject: [PATCH 049/150] fix(analyze-boot): use monotonic clock for containers #6322 Fixes GH-5773 --- cloudinit/analyze/show.py | 95 ++++++++++++----- tests/integration_tests/cmd/test_analyze.py | 36 +++++++ tests/integration_tests/util.py | 29 ++++++ tests/unittests/analyze/test_boot.py | 107 ++++++++++++++++++-- 4 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 tests/integration_tests/cmd/test_analyze.py diff --git a/cloudinit/analyze/show.py b/cloudinit/analyze/show.py index e9a8c7a1472..b4491bca6cd 100644 --- a/cloudinit/analyze/show.py +++ b/cloudinit/analyze/show.py @@ -121,11 +121,17 @@ class SystemctlReader: """ def __init__(self, property, parameter=None): - self.epoch = None + self.stdout = None self.args = [subp.which("systemctl"), "show"] if parameter: self.args.append(parameter) - self.args.extend(["-p", property]) + # --timestamp=utc is needed for native date strings. Othwerise, + # the datetime will be returned in the local timezone (which would be + # a problem for strptime used later on in this method) + # This option does not affect monotonic properties (values as + # microsecond int) + self.args.extend(["-p", property, "--timestamp=us+utc"]) + # Don't want the init of our object to break. Instead of throwing # an exception, set an error code that gets checked when data is # requested from the object @@ -142,12 +148,12 @@ def subp(self): value, err = subp.subp(self.args, capture=True) if err: return err - self.epoch = value + self.stdout = value return None except Exception as systemctl_fail: return systemctl_fail - def parse_epoch_as_float(self): + def convert_val_to_float(self): """ If subp call succeeded, return the timestamp from subp as a float. @@ -162,10 +168,27 @@ def parse_epoch_as_float(self): "returning error code ({})".format(self.failure) ) # Output from systemctl show has the format Property=Value. - # For example, UserspaceMonotonic=1929304 - timestamp = self.epoch.split("=")[1] - # Timestamps reported by systemctl are in microseconds, converting - return float(timestamp) / 1000000 + val = self.stdout.split("=")[1].strip() + + if val.isnumeric(): + # Float Timestamps reported by systemctl are in + # microseconds, converting to seconds + # For example, UserspaceMonotonic=1929304 + timestamp = float(val) / 1000000 + else: + # The format in this case is always "%a %Y-%m-%d %H:%M:%S %Z" + # For example, UserspaceTimestamp=Wed 2025-07-30 05:14:32 UTC + + # strptime returns a naive datetime so we need to explictly + # set the timezone of this datetime + # at the timezone of the parsed string (utc) + timestamp = ( + datetime.datetime.strptime(val, "%a %Y-%m-%d %H:%M:%S.%f %Z") + .replace(tzinfo=datetime.timezone.utc) + .timestamp() + ) + + return timestamp def dist_check_timestamp(): @@ -226,24 +249,40 @@ def gather_timestamps_using_systemd(): Gather timestamps that corresponds to kernel begin initialization, kernel finish initialization. and cloud-init systemd unit activation - :return: the three timestamps + :return: the three timesread_propertystamps """ - kernel_start = float(time.time()) - float(util.uptime()) try: - delta_k_end = SystemctlReader( - "UserspaceTimestampMonotonic" - ).parse_epoch_as_float() - delta_ci_s = SystemctlReader( - "InactiveExitTimestampMonotonic", "cloud-init-local" - ).parse_epoch_as_float() - base_time = kernel_start - status = SUCCESS_CODE - # lxc based containers do not set their monotonic zero point to be when - # the container starts, instead keep using host boot as zero point + # The use of the monotonic timestamps is needed in cloud-init-related + # dates to account for the 2-second delay when cloud-init sets up NTP if util.is_container(): - status = CONTAINER_CODE - kernel_end = base_time + delta_k_end - cloudinit_sysd = base_time + delta_ci_s + # lxc based containers do not set their monotonic zero point to be + # when the container starts, + # instead keep using host boot as zero point + kernel_start = SystemctlReader( + "UserspaceTimestamp" + ).convert_val_to_float() + monotonic_offset = SystemctlReader( + "UserspaceTimestampMonotonic" + ).convert_val_to_float() + else: + kernel_start = SystemctlReader( + "KernelTimestamp" + ).convert_val_to_float() + monotonic_offset = SystemctlReader( + "KernelTimestampMonotonic" + ).convert_val_to_float() + kernel_end = ( + SystemctlReader( + "UserspaceTimestampMonotonic" + ).convert_val_to_float() + - monotonic_offset + ) + cloudinit_sysd = ( + SystemctlReader( + "InactiveExitTimestampMonotonic", "cloud-init-local" + ).convert_val_to_float() + - monotonic_offset + ) except Exception as e: # Except ALL exceptions as Systemctl reader can throw many different @@ -251,7 +290,15 @@ def gather_timestamps_using_systemd(): # obtained print(e) return TIMESTAMP_UNKNOWN - return status, kernel_start, kernel_end, cloudinit_sysd + + status = CONTAINER_CODE if util.is_container() else SUCCESS_CODE + + return ( + status, + kernel_start, + kernel_start + kernel_end, + kernel_start + cloudinit_sysd, + ) def generate_records( diff --git a/tests/integration_tests/cmd/test_analyze.py b/tests/integration_tests/cmd/test_analyze.py new file mode 100644 index 00000000000..ceee47e425a --- /dev/null +++ b/tests/integration_tests/cmd/test_analyze.py @@ -0,0 +1,36 @@ +# This file is part of cloud-init. See LICENSE file for license information. +"""Tests for `cloud-init analyze`""" +import pytest + +from cloudinit.distros import uses_systemd +from tests.integration_tests.integration_settings import PLATFORM +from tests.integration_tests.util import get_datetime_from_string + + +class TestAnalyzeCommand: + @pytest.mark.skipif(not uses_systemd(), reason="Relies on systemd output") + @pytest.mark.skipif( + PLATFORM != "lxd_container", + reason="Testing lxdcontainer-specific behavior", + ) + def test_analyze_boot_ordered_timestamps(self, module_client): + """ + Confirm that analyze boot is working correctly in lxd containers + and that the correct zero-point is used for the monotonic clock used + to determine when cloud-init was activated by systemd + """ + assert module_client.execute("cloud-init status --wait --long").ok + result = module_client.execute("cloud-init analyze boot") + assert result.stderr == "container" + + container_start_time = get_datetime_from_string( + result.stdout, "^\\s*Container started at: (.+?)$" + ) + cloudinit_activation_time = get_datetime_from_string( + result.stdout, "^\\s*Cloud-init activated by systemd at: (.+?)$" + ) + cloudinit_start_time = get_datetime_from_string( + result.stdout, "^\\s*Cloud-init start: (.+?)$" + ) + assert container_start_time < cloudinit_activation_time + assert cloudinit_activation_time < cloudinit_start_time diff --git a/tests/integration_tests/util.py b/tests/integration_tests/util.py index 270922d490b..6e86b677552 100644 --- a/tests/integration_tests/util.py +++ b/tests/integration_tests/util.py @@ -6,6 +6,7 @@ import time from collections import namedtuple from contextlib import contextmanager +from datetime import datetime from functools import lru_cache from itertools import chain from pathlib import Path @@ -663,3 +664,31 @@ def network_wait_logged(log: str) -> bool: "Running command " "['systemctl', 'start', 'systemd-networkd-wait-online.service']" ) in log + + +def get_datetime_from_string( + str, regex, datetime_strformat="%Y-%m-%d %H:%M:%S.%f%z" +): + """ + Extract datetime from a given line in a string + """ + matched = re.search(regex, str, re.M) + assert matched, ( + f"Unable to find the datetime using the regex {regex}", + f"inside the string {str}", + ) + + try: + converted_datetime = datetime.strptime( + matched.group(1), datetime_strformat + ) + except ValueError: + pytest.fail( + " ".join( + ( + f"Unable to parse the datetime {matched.group(1)}", + f"using the format {datetime_strformat}", + ) + ) + ) + return converted_datetime diff --git a/tests/unittests/analyze/test_boot.py b/tests/unittests/analyze/test_boot.py index c25cc8a7b7c..18911c96ff9 100644 --- a/tests/unittests/analyze/test_boot.py +++ b/tests/unittests/analyze/test_boot.py @@ -6,8 +6,10 @@ from cloudinit.analyze.show import ( CONTAINER_CODE, FAIL_CODE, + SUCCESS_CODE, SystemctlReader, dist_check_timestamp, + gather_timestamps_using_systemd, ) from tests.unittests.helpers import mock @@ -37,28 +39,36 @@ def test_systemctl_invalid(self, mocker): ) reader = SystemctlReader("dont", "care") with pytest.raises(RuntimeError): - reader.parse_epoch_as_float() + reader.convert_val_to_float() @mock.patch("cloudinit.subp.subp", return_value=("U=1000000", None)) def test_systemctl_works_correctly_threshold(self, m_subp): reader = SystemctlReader("dummyProperty", "dummyParameter") - assert 1.0 == reader.parse_epoch_as_float() - thresh = 1.0 - reader.parse_epoch_as_float() + assert 1.0 == reader.convert_val_to_float() + thresh = 1.0 - reader.convert_val_to_float() assert thresh < 1e-6 assert thresh > (-1 * 1e-6) @mock.patch("cloudinit.subp.subp", return_value=("U=0", None)) def test_systemctl_succeed_zero(self, m_subp): reader = SystemctlReader("dummyProperty", "dummyParameter") - assert 0.0 == reader.parse_epoch_as_float() + assert 0.0 == reader.convert_val_to_float() + + @mock.patch( + "cloudinit.subp.subp", + return_value=("U=Fri 1970-01-02 00:01:15.123 UTC", None), + ) + def test_systemctl_succeed_human_readable_date(self, m_subp): + reader = SystemctlReader("dummyProperty", "dummyParameter") + assert 86475.123 == reader.convert_val_to_float() @mock.patch("cloudinit.subp.subp", return_value=("U=1", None)) def test_systemctl_succeed_distinct(self, m_subp): reader = SystemctlReader("dummyProperty", "dummyParameter") - val1 = reader.parse_epoch_as_float() + val1 = reader.convert_val_to_float() m_subp.return_value = ("U=2", None) reader2 = SystemctlReader("dummyProperty", "dummyParameter") - val2 = reader2.parse_epoch_as_float() + val2 = reader2.convert_val_to_float() assert val1 != val2 @pytest.mark.parametrize( @@ -77,7 +87,7 @@ def test_systemctl_epoch_not_error(self, m_subp, return_value, exception): m_subp.return_value = return_value reader = SystemctlReader("dummyProperty", "dummyParameter") with pytest.raises(exception): - reader.parse_epoch_as_float() + reader.convert_val_to_float() class TestAnalyzeBoot: @@ -181,3 +191,86 @@ def test_container_ci_log_line(self, m_is_container, m_subp, m_get, m_g): self.remove_dummy_file(path, log_path) assert CONTAINER_CODE == finish_code + + @mock.patch("cloudinit.analyze.show.SystemctlReader") + @pytest.mark.parametrize( + ( + "is_container_returnvalue", + "expected_first_2_constructors_call_list", + "m_returns_of_convert_call", + "expected_return", + ), + ( + ( + True, + [ + mock.call("UserspaceTimestamp"), + mock.call("UserspaceTimestampMonotonic"), + ], + [1500, 7, 7, 18], + (CONTAINER_CODE, 1500, 1500, 1511), + ), + ( + False, + [ + mock.call("KernelTimestamp"), + mock.call("KernelTimestampMonotonic"), + ], + [1000, 1, 3, 8], + (SUCCESS_CODE, 1000, 1002, 1007), + ), + ), + ) + def test_gather_timestamps_using_systemd( + self, + mock_SystemctlReader, + is_container_returnvalue, + expected_first_2_constructors_call_list, + m_returns_of_convert_call, + expected_return, + mocker, + ): + """ + Testing the behavior based on whether or not the + instance is a container + """ + mocker.patch( + "cloudinit.util.is_container", + return_value=is_container_returnvalue, + ) + + # Mocking the return values of the 4 calls to convert_val_to_float + # in gather_timestamps_using_systemd + mock_SystemctlReader_instances = [mock.Mock() for _ in range(4)] + for instance, value in zip( + mock_SystemctlReader_instances, m_returns_of_convert_call + ): + instance.convert_val_to_float.return_value = value + mock_SystemctlReader.side_effect = mock_SystemctlReader_instances + + assert expected_return == gather_timestamps_using_systemd() + + # Verifying that the 4 constructor calls of SystemctlReader in + # gather_timestamps_using_systemd were with the expected arguments + assert ( + mock_SystemctlReader.call_args_list[:2] + == expected_first_2_constructors_call_list + ) + assert mock_SystemctlReader.call_args_list[2:] == [ + mock.call("UserspaceTimestampMonotonic"), + mock.call("InactiveExitTimestampMonotonic", "cloud-init-local"), + ] + + @mock.patch( + "cloudinit.analyze.show.SystemctlReader", + side_effect=Exception("ARandomError"), + ) + @mock.patch("cloudinit.subp.subp") + def test_gather_timestamps_using_systemd_with_SystemctlReader_exception( + self, m_subp, systemctlReader_mock + ): + """ + Confirm the function returns the error code when SystemctlReader + raises an exception + """ + assert gather_timestamps_using_systemd() == err_code From 01b93d065bc793093e5dfe73dcdd764e2e659f21 Mon Sep 17 00:00:00 2001 From: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com> Date: Sat, 2 Aug 2025 03:57:00 +0530 Subject: [PATCH 050/150] feat(networkd): support vlan and bond rendering (#6324) Networkd renderer used in Photon OS doesn't support vlan and bond settings specified through network config yaml. This is an effort towards supporting the same in networkd renderer. Signed-off-by: Shreenidhi Shedi --- cloudinit/net/networkd.py | 334 ++++++++++++++++-- tests/unittests/cmd/devel/test_net_convert.py | 1 - .../network/10-cloud-init-bond.200.netdev | 8 + .../network/10-cloud-init-bond.200.network | 14 + .../network/10-cloud-init-bond0.netdev | 11 + .../network/10-cloud-init-bond0.network | 10 + .../network/10-cloud-init-eth0.network | 11 + .../network/10-cloud-init-eth1.network | 5 + .../network/10-cloud-init-eth2.network | 16 + .../network/10-cloud-init-vl101.netdev | 6 + .../network/10-cloud-init-vl101.network | 5 + .../network/10-cloud-init-vlan100.netdev | 8 + .../network/10-cloud-init-vlan100.network | 27 ++ .../net/artifacts/photon_net_config_v2.yaml | 86 +++++ tests/unittests/net/network_configs.py | 4 +- tests/unittests/net/test_net_rendering.py | 48 ++- 16 files changed, 543 insertions(+), 51 deletions(-) create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth1.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network create mode 100644 tests/unittests/net/artifacts/photon_net_config_v2.yaml diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py index 64c1925296a..8672e56e364 100644 --- a/cloudinit/net/networkd.py +++ b/cloudinit/net/networkd.py @@ -1,12 +1,11 @@ -# Copyright (C) 2021-2022 VMware Inc. +# Copyright (C) 2021-2025 VMware by Broadcom. # # Author: Shreenidhi Shedi # # This file is part of cloud-init. See LICENSE file for license information. import logging -from collections import OrderedDict -from typing import Optional +from typing import Any, Dict, List, Optional from cloudinit import subp, util from cloudinit.net import renderer, should_add_gateway_onlink_flag @@ -15,25 +14,64 @@ LOG = logging.getLogger(__name__) +def normalize(data: Dict[str, List[Any]]) -> Dict[str, List[Any]]: + """ + Normalize a dictionary of lists. + - Assumes top-level keys map to lists. + - Each list and any nested dicts/lists will be recursively normalized. + """ + normalized = {} + for key, value in data.items(): + normalized[key] = _normalize_value(value) + return normalized + + +def _normalize_value(data: Any) -> Any: + """ + Recursively normalize a dictionary or list: + - Dicts: keys sorted, values normalized + - Lists: items normalized and sorted (if comparable) + """ + if isinstance(data, dict): + normalized = {} + for key in sorted(data): + normalized[key] = _normalize_value(data[key]) + return normalized + elif isinstance(data, list): + normalized_items = [] + for item in data: + if isinstance(item, (dict, list)): + normalized_item = _normalize_value(item) + else: + normalized_item = item + normalized_items.append(normalized_item) + try: + return sorted(normalized_items) + except TypeError: + return normalized_items + + return data + + class CfgParser: def __init__(self): - self.conf_dict = OrderedDict( - { - "Match": [], - "Link": [], - "Network": [], - "DHCPv4": [], - "DHCPv6": [], - "Address": [], - "Route": {}, - } - ) + self.conf_dict = { + "Match": [], + "Link": [], + "Network": [], + "DHCPv4": [], + "DHCPv6": [], + "Address": [], + "Route": {}, + "NetDev": [], + "VLAN": [], + "Bond": [], + } def update_section(self, sec, key, val): for k in self.conf_dict.keys(): if k == sec: - self.conf_dict[k].append(key + "=" + str(val)) - # remove duplicates from list + self.conf_dict[k].append(f"{key}={val}") self.conf_dict[k] = list(dict.fromkeys(self.conf_dict[k])) self.conf_dict[k].sort() @@ -46,7 +84,7 @@ def update_route_section(self, sec, rid, key, val): if k == sec: if rid not in self.conf_dict[k]: self.conf_dict[k][rid] = [] - self.conf_dict[k][rid].append(key + "=" + str(val)) + self.conf_dict[k][rid].append(f"{key}={val}") # remove duplicates from list self.conf_dict[k][rid] = list( dict.fromkeys(self.conf_dict[k][rid]) @@ -55,24 +93,25 @@ def update_route_section(self, sec, rid, key, val): def get_final_conf(self): contents = "" + + self.conf_dict = normalize(self.conf_dict) + for k, v in sorted(self.conf_dict.items()): if not v: continue if k == "Address": for e in sorted(v): - contents += "[" + k + "]\n" - contents += e + "\n" - contents += "\n" + contents += f"[{k}]\n{e}\n\n" elif k == "Route": for n in sorted(v): - contents += "[" + k + "]\n" + contents += f"[{k}]\n" for e in sorted(v[n]): - contents += e + "\n" + contents += f"{e}\n" contents += "\n" else: - contents += "[" + k + "]\n" + contents += f"[{k}]\n" for e in sorted(v): - contents += e + "\n" + contents += f"{e}\n" contents += "\n" return contents @@ -101,9 +140,11 @@ def generate_match_section(self, iface, cfg: CfgParser): match_dict = { "name": "Name", "driver": "Driver", - "mac_address": "MACAddress", } + if iface["type"] == "physical": + match_dict["mac_address"] = "MACAddress" + if not iface: return @@ -119,9 +160,12 @@ def generate_link_section(self, iface, cfg: CfgParser): if not iface: return - if "mtu" in iface and iface["mtu"]: + if iface.get("mtu"): cfg.update_section(sec, "MTUBytes", iface["mtu"]) + if iface["type"] != "physical" and iface.get("mac_address"): + cfg.update_section(sec, "MACAddress", iface["mac_address"]) + if "optional" in iface and iface["optional"]: cfg.update_section(sec, "RequiredForOnline", "no") @@ -140,7 +184,7 @@ def parse_routes(self, rid, conf, cfg: CfgParser): # prefix is derived using netmask by network_state prefix = "" if "prefix" in conf: - prefix = "/" + str(conf["prefix"]) + prefix = f"/{conf['prefix']}" for k, v in conf.items(): if k not in route_cfg_map: @@ -155,7 +199,7 @@ def parse_subnets(self, iface, cfg: CfgParser): rid = 0 for e in iface.get("subnets", []): t = e["type"] - if t == "dhcp4" or t == "dhcp": + if t in {"dhcp4", "dhcp"}: if dhcp == "no": dhcp = "ipv4" elif dhcp == "ipv6": @@ -174,7 +218,7 @@ def parse_subnets(self, iface, cfg: CfgParser): if "address" in e: addr = e["address"] if "prefix" in e: - addr += "/" + str(e["prefix"]) + addr += f"/{e['prefix']}" subnet_cfg_map = { "address": "Address", "gateway": "Gateway", @@ -201,13 +245,16 @@ def parse_subnets(self, iface, cfg: CfgParser): "Route", f"a{rid}", "GatewayOnLink", "yes" ) rid = rid + 1 - elif k == "dns_nameservers" or k == "dns_search": + elif k in {"dns_nameservers", "dns_search"}: cfg.update_section(sec, subnet_cfg_map[k], " ".join(v)) cfg.update_section(sec, "DHCP", dhcp) - if isinstance(iface.get("accept-ra", ""), bool): - cfg.update_section(sec, "IPv6AcceptRA", iface["accept-ra"]) + if isinstance(iface.get("accept-ra"), bool): + val = "no" + if iface["accept-ra"]: + val = "yes" + cfg.update_section(sec, "IPv6AcceptRA", val) return dhcp @@ -275,12 +322,14 @@ def parse_dhcp_overrides(self, cfg: CfgParser, device, dhcp, version): if v in dhcp_overrides: cfg.update_section(f"DHCPv{version}", k, dhcp_overrides[v]) - def create_network_file(self, link, conf, nwk_dir): + def create_network_file(self, link, conf, nwk_dir, ext=".network"): net_fn_owner = "systemd-network" LOG.debug("Setting Networking Config for %s", link) + net_fn = f"{nwk_dir}10-cloud-init-{link}{ext}" - net_fn = nwk_dir + "10-cloud-init-" + link + ".network" + if conf.endswith("\n\n"): + conf = conf[:-1] util.write_file(net_fn, conf) util.chownbyname(net_fn, net_fn_owner, net_fn_owner) @@ -296,16 +345,65 @@ def render_network_state( util.ensure_dir(network_dir) - ret_dict = self._render_content(network_state) - for k, v in ret_dict.items(): + network = self._render_content(network_state) + vlan_netdev = network.pop("vlan_netdev", {}) + bond_netdev = network.pop("bond_netdev", {}) + + for k, v in network.items(): self.create_network_file(k, v, network_dir) - def _render_content(self, ns: NetworkState) -> dict: + for k, v in vlan_netdev.items(): + self.create_network_file(k, v, network_dir, ext=".netdev") + + for k, v in bond_netdev.items(): + self.create_network_file(k, v, network_dir, ext=".netdev") + + def _render_content(self, ns: NetworkState): ret_dict = {} + vlan_link = {} + bond_link = {} + + if "vlans" in ns.config: + vlan_dict = self.render_vlans(ns) + vlan_netdev = vlan_dict["vlan_netdev"] + vlan_link = vlan_dict["vlan_link"] + ret_dict["vlan_netdev"] = vlan_netdev + + if "bonds" in ns.config: + bond_dict = self.render_bonds(ns) + bond_netdev = bond_dict["bond_netdev"] + bond_link = bond_dict["bond_link"] + ret_dict["bond_netdev"] = bond_netdev + for iface in ns.iter_interfaces(): cfg = CfgParser() + iface_name = iface["name"] + + vlan_link_name = vlan_link.get(iface_name) + if vlan_link_name: + cfg.update_section("Network", "VLAN", vlan_link_name) + + # TODO: revisit this once network state renders macaddress + # properly for vlan config + if not iface["mac_address"] and vlan_link.get("macaddress"): + mac = vlan_link["macaddress"].get(iface_name) + if mac: + iface["mac_address"] = mac + + bond_link_name = bond_link.get(iface_name) + if bond_link_name: + cfg.update_section("Network", "Bond", bond_link_name) + + # TODO: revisit this once network state renders macaddress + # properly for bond config + if not iface["mac_address"] and bond_link.get("macaddress"): + mac = bond_link["macaddress"].get(iface_name) + if mac: + iface["mac_address"] = mac + link = self.generate_match_section(iface, cfg) + self.generate_link_section(iface, cfg) dhcp = self.parse_subnets(iface, cfg) self.parse_dns(iface, cfg, ns) @@ -358,6 +456,168 @@ def _render_content(self, ns: NetworkState) -> dict: return ret_dict + def render_vlans(self, ns: NetworkState) -> dict: + vlan_link_info: Dict[str, Any] = {} + vlan_ndev_configs = {} + vlan_link_info["macaddress"] = {} + + vlans = ns.config.get("vlans", {}) + for vlan_name, vlan_cfg in vlans.items(): + vlan_id = vlan_cfg.get("id") + parent = vlan_cfg.get("link") + + if vlan_id is None or parent is None: + LOG.warning( + "Skipping VLAN %s - missing 'id' or 'link'", vlan_name + ) + continue + + vlan_link_info[parent] = vlan_name + + # -------- .netdev for VLAN -------- + cfg = CfgParser() + cfg.update_section("NetDev", "Name", vlan_name) + cfg.update_section("NetDev", "Kind", "vlan") + + val = vlan_cfg.get("mtu") + if val: + cfg.update_section("NetDev", "MTUBytes", val) + + val = vlan_cfg.get("macaddress") + if val: + val = val.lower() + cfg.update_section("NetDev", "MACAddress", val) + vlan_link_info["macaddress"][vlan_name] = val + + cfg.update_section("VLAN", "Id", vlan_id) + vlan_ndev_configs[vlan_name] = cfg.get_final_conf() + + ret_dict = { + "vlan_netdev": vlan_ndev_configs, + "vlan_link": vlan_link_info, + } + return ret_dict + + def render_bonds(self, ns: NetworkState) -> dict: + bond_link_info: Dict[str, Any] = {} + bond_ndev_configs = {} + section = "Bond" + + bond_link_info["macaddress"] = {} + + bonds = ns.config.get("bonds", {}) + for bond_name, bond_cfg in bonds.items(): + interfaces = bond_cfg.get("interfaces") + if not interfaces: + LOG.warning( + "Skipping bond %s - missing 'interfaces'", bond_name + ) + continue + + bond_link_info.update({iface: bond_name for iface in interfaces}) + + # -------- .netdev for Bond -------- + cfg = CfgParser() + cfg.update_section("NetDev", "Name", bond_name) + cfg.update_section("NetDev", "Kind", "bond") + + val = bond_cfg.get("mtu") + if val: + cfg.update_section("NetDev", "MTUBytes", val) + + val = bond_cfg.get("macaddress") + if val: + val = val.lower() + cfg.update_section("NetDev", "MACAddress", val) + bond_link_info["macaddress"][bond_name] = val + + # Optional bond parameters + params = bond_cfg.get("parameters", {}) + + if "mode" in params: + cfg.update_section(section, "Mode", params["mode"]) + + if "mii-monitor-interval" in params: + cfg.update_section( + section, + "MIIMonitorSec", + f"{params['mii-monitor-interval']}ms", + ) + + if "updelay" in params: + cfg.update_section( + section, "UpDelaySec", f"{params['updelay']}ms" + ) + + if "downdelay" in params: + cfg.update_section( + section, "DownDelaySec", f"{params['downdelay']}ms" + ) + + if "arp-interval" in params: + cfg.update_section( + section, "ARPIntervalSec", f"{params['arp-interval']}ms" + ) + + if "arp-ip-target" in params: + targets = params["arp-ip-target"] + if isinstance(targets, str): + targets = [targets] + ip_list = " ".join(targets) + cfg.update_section(section, "ARPIPTargets", ip_list) + + if "arp-validate" in params: + cfg.update_section( + section, "ARPValidate", params["arp-validate"] + ) + + if "arp-all-targets" in params: + cfg.update_section( + section, "ARPAllTargets", params["arp-all-targets"] + ) + + if "primary-reselect" in params: + cfg.update_section( + section, + "PrimaryReselectPolicy", + params["primary-reselect"], + ) + + if "lacp-rate" in params: + cfg.update_section( + section, "LACPTransmitRate", params["lacp-rate"] + ) + + if "transmit-hash-policy" in params: + cfg.update_section( + section, + "TransmitHashPolicy", + params["transmit-hash-policy"], + ) + + if "ad-select" in params: + cfg.update_section(section, "AdSelect", params["ad-select"]) + + if "min-links" in params: + cfg.update_section( + section, "MinLinks", str(params["min-links"]) + ) + + if "all-slaves-active" in params: + cfg.update_section( + section, + "AllSlavesActive", + str(params["all-slaves-active"]).lower(), + ) + + bond_ndev_configs[bond_name] = cfg.get_final_conf() + + ret_dict = { + "bond_netdev": bond_ndev_configs, + "bond_link": bond_link_info, + } + return ret_dict + def available(target=None): expected = ["ip", "systemctl"] diff --git a/tests/unittests/cmd/devel/test_net_convert.py b/tests/unittests/cmd/devel/test_net_convert.py index 39e049227b5..9328150d5bc 100644 --- a/tests/unittests/cmd/devel/test_net_convert.py +++ b/tests/unittests/cmd/devel/test_net_convert.py @@ -54,7 +54,6 @@ [Network] DHCP=ipv4 - """ SAMPLE_SYSCONFIG_CONTENT = """\ diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.netdev new file mode 100644 index 00000000000..c82fd6f5394 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.netdev @@ -0,0 +1,8 @@ +[NetDev] +Kind=vlan +MACAddress=00:11:22:33:44:99 +MTUBytes=111 +Name=bond.200 + +[VLAN] +Id=200 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.network new file mode 100644 index 00000000000..6456336478c --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond.200.network @@ -0,0 +1,14 @@ +[Address] +Address=192.168.200.10/24 + +[Link] +MACAddress=00:11:22:33:44:99 +MTUBytes=111 + +[Match] +Name=bond.200 + +[Network] +DHCP=ipv6 +DNS=1.1.1.1 8.8.4.4 +Domains=bond.vlan.test diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.netdev new file mode 100644 index 00000000000..dd2da41dc16 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.netdev @@ -0,0 +1,11 @@ +[Bond] +LACPTransmitRate=fast +MIIMonitorSec=100ms +Mode=802.3ad +TransmitHashPolicy=layer2+3 + +[NetDev] +Kind=bond +MACAddress=00:11:22:33:44:77 +MTUBytes=333 +Name=bond0 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.network new file mode 100644 index 00000000000..bbebbf26da3 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-bond0.network @@ -0,0 +1,10 @@ +[Link] +MACAddress=00:11:22:33:44:77 +MTUBytes=333 + +[Match] +Name=bond0 + +[Network] +DHCP=no +VLAN=bond.200 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network new file mode 100644 index 00000000000..e3a8358fd7a --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network @@ -0,0 +1,11 @@ +[Link] +MTUBytes=1500 + +[Match] +MACAddress=00:11:22:33:44:55 +Name=eth0 + +[Network] +Bond=bond0 +DHCP=no +VLAN=vlan100 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth1.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth1.network new file mode 100644 index 00000000000..19e5db2fedc --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth1.network @@ -0,0 +1,5 @@ +[Match] +Name=eth1 + +[Network] +DHCP=yes diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network new file mode 100644 index 00000000000..712c52b71b6 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network @@ -0,0 +1,16 @@ +[Address] +Address=10.20.30.40/24 + +[Link] +MTUBytes=1400 + +[Match] +MACAddress=00:aa:bb:cc:dd:ee +Name=eth2 + +[Network] +Bond=bond0 +DHCP=no +DNS=9.9.9.9 8.8.8.8 +Domains=example.org +VLAN=vl101 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.netdev new file mode 100644 index 00000000000..3fa9ff336b2 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.netdev @@ -0,0 +1,6 @@ +[NetDev] +Kind=vlan +Name=vl101 + +[VLAN] +Id=101 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network new file mode 100644 index 00000000000..fb82925b4e6 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network @@ -0,0 +1,5 @@ +[Match] +Name=vl101 + +[Network] +DHCP=no diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.netdev new file mode 100644 index 00000000000..5c6a1c097fe --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.netdev @@ -0,0 +1,8 @@ +[NetDev] +Kind=vlan +MACAddress=00:11:22:33:44:66 +MTUBytes=901 +Name=vlan100 + +[VLAN] +Id=100 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network new file mode 100644 index 00000000000..f8afc4724f9 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network @@ -0,0 +1,27 @@ +[Address] +Address=192.168.100.10/24 + +[Address] +Address=192.168.100.11/24 + +[Link] +MACAddress=00:11:22:33:44:66 +MTUBytes=901 + +[Match] +Name=vlan100 + +[Network] +DHCP=ipv6 +DNS=8.8.8.8 1.1.1.1 +Domains=corp.example.com vlan.test + +[Route] +Destination=10.10.200.0/24 +Gateway=192.168.100.1 +Metric=50 + +[Route] +Destination=10.10.201.0/24 +Gateway=192.168.100.2 +Metric=150 diff --git a/tests/unittests/net/artifacts/photon_net_config_v2.yaml b/tests/unittests/net/artifacts/photon_net_config_v2.yaml new file mode 100644 index 00000000000..7984f8438fe --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config_v2.yaml @@ -0,0 +1,86 @@ +network: + version: 2 + + ethernets: + eth0: + match: + macaddress: "00:11:22:33:44:55" + set-name: eth0 + dhcp4: false + mtu: 1500 + + eth1: + dhcp4: true + dhcp6: true + + eth2: + match: + macaddress: "00:aa:bb:cc:dd:ee" + set-name: eth2 + mtu: 1400 + dhcp4: false + addresses: + - 10.20.30.40/24 + nameservers: + addresses: + - 9.9.9.9 + - 8.8.8.8 + search: + - example.org + + bonds: + bond0: + interfaces: [eth0, eth2] + parameters: + mode: 802.3ad + mii-monitor-interval: 100 + transmit-hash-policy: layer2+3 + lacp-rate: fast + dhcp4: false + dhcp6: false + mtu: 333 + macaddress: "00:11:22:33:44:77" + + vlans: + vlan100: + id: 100 + link: eth0 + addresses: + - 192.168.100.10/24 + - 192.168.100.11/24 + dhcp6: true + mtu: 901 + macaddress: "00:11:22:33:44:66" + nameservers: + addresses: + - 8.8.8.8 + - 1.1.1.1 + search: + - corp.example.com + - vlan.test + routes: + - to: 10.10.200.0/24 + via: 192.168.100.1 + metric: 50 + - to: 10.10.201.0/24 + via: 192.168.100.2 + metric: 150 + + vl101: + id: 101 + link: eth2 + + bond.200: + id: 200 + link: bond0 + macaddress: "00:11:22:33:44:99" + addresses: + - 192.168.200.10/24 + nameservers: + addresses: + - 1.1.1.1 + - 8.8.4.4 + search: + - bond.vlan.test + mtu: 111 + dhcp6: true diff --git a/tests/unittests/net/network_configs.py b/tests/unittests/net/network_configs.py index 6e870f6ede5..2fdfc73baaf 100644 --- a/tests/unittests/net/network_configs.py +++ b/tests/unittests/net/network_configs.py @@ -1000,7 +1000,7 @@ Name=iface0 [Network] DHCP=ipv6 - IPv6AcceptRA=True + IPv6AcceptRA=yes """ ).rstrip(" "), }, @@ -1075,7 +1075,7 @@ Name=iface0 [Network] DHCP=ipv6 - IPv6AcceptRA=False + IPv6AcceptRA=no """ ).rstrip(" "), }, diff --git a/tests/unittests/net/test_net_rendering.py b/tests/unittests/net/test_net_rendering.py index 0f3c766fdeb..72fe3b1b167 100644 --- a/tests/unittests/net/test_net_rendering.py +++ b/tests/unittests/net/test_net_rendering.py @@ -35,6 +35,8 @@ from cloudinit.net.netplan import Renderer as NetplanRenderer from cloudinit.net.network_manager import Renderer as NetworkManagerRenderer from cloudinit.net.network_state import NetworkState, parse_net_config_data +from cloudinit.net.networkd import Renderer as NetworkdRenderer +from tests.unittests.helpers import mock ARTIFACT_DIR = Path(__file__).parent.absolute() / "artifacts" @@ -50,6 +52,19 @@ def setup(mocker): mocker.patch("cloudinit.net.network_state.get_interfaces_by_mac") +def _check_file_diff(expected_paths: list, tmp_path: Path): + for expected_path in expected_paths: + expected_contents = Path(expected_path).read_text() + actual_path = tmp_path / expected_path.split( + str(ARTIFACT_DIR), maxsplit=1 + )[1].lstrip("/") + assert ( + actual_path.exists() + ), f"Expected {actual_path} to exist, but it does not" + actual_contents = actual_path.read_text() + assert expected_contents.strip() == actual_contents.strip() + + def _check_netplan( network_state: NetworkState, netplan_path: Path, expected_config ): @@ -73,21 +88,30 @@ def _check_network_manager(network_state: NetworkState, tmp_path: Path): str(ARTIFACT_DIR / "no_matching_mac" / "**/*.nmconnection"), recursive=True, ) - for expected_path in expected_paths: - expected_contents = Path(expected_path).read_text() - actual_path = tmp_path / expected_path.split( - str(ARTIFACT_DIR), maxsplit=1 - )[1].lstrip("/") - assert ( - actual_path.exists() - ), f"Expected {actual_path} to exist, but it does not" - actual_contents = actual_path.read_text() - assert expected_contents.strip() == actual_contents.strip() + _check_file_diff(expected_paths, tmp_path) + + +@mock.patch("cloudinit.net.util.chownbyname", return_value=True) +def _check_networkd_renderer( + network_state: NetworkState, tmp_path: Path, m_chown +): + renderer = NetworkdRenderer() + renderer.render_network_state( + network_state, target=str(tmp_path / "photon_net_config") + ) + expected_paths = glob.glob( + str(ARTIFACT_DIR / "photon_net_config" / "**/*.net*"), + recursive=True, + ) + _check_file_diff(expected_paths, tmp_path) @pytest.mark.parametrize( "test_name, renderers", - [("no_matching_mac_v2", Renderer.Netplan | Renderer.NetworkManager)], + [ + ("no_matching_mac_v2", Renderer.Netplan | Renderer.NetworkManager), + ("photon_net_config_v2", Renderer.Networkd), + ], ) def test_convert(test_name, renderers, tmp_path): network_config = yaml.safe_load( @@ -100,3 +124,5 @@ def test_convert(test_name, renderers, tmp_path): ) if Renderer.NetworkManager in renderers: _check_network_manager(network_state, tmp_path) + if Renderer.Networkd in renderers: + _check_networkd_renderer(network_state, tmp_path) From 219cbbbb85256c349c5c9ec16560a15e01879644 Mon Sep 17 00:00:00 2001 From: abdulganiyy Date: Mon, 4 Aug 2025 11:13:29 +0100 Subject: [PATCH 051/150] fix: fixed defs types in cloudinit analyze dump (#6343) --- cloudinit/analyze/dump.py | 24 ++++++++++++++---------- pyproject.toml | 1 - 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/cloudinit/analyze/dump.py b/cloudinit/analyze/dump.py index 55d149c4432..dd1b9c32838 100644 --- a/cloudinit/analyze/dump.py +++ b/cloudinit/analyze/dump.py @@ -3,10 +3,11 @@ import calendar import sys from datetime import datetime, timezone +from typing import IO, Any, Dict, List, Optional, TextIO, Tuple from cloudinit import atomic_helper, subp, util -stage_to_description = { +stage_to_description: Dict[str, str] = { "finished": "finished running cloud-init", "init-local": "starting search for local datasources", "init-network": "searching for network datasources", @@ -27,7 +28,7 @@ DEFAULT_FMT = "%b %d %H:%M:%S %Y" -def parse_timestamp(timestampstr): +def parse_timestamp(timestampstr: str) -> float: # default syslog time does not include the current year months = [calendar.month_abbr[m] for m in range(1, 13)] if timestampstr.split()[0] in months: @@ -54,7 +55,7 @@ def parse_timestamp(timestampstr): return float(timestamp) -def has_gnu_date(): +def has_gnu_date() -> bool: """GNU date includes a string containing the word GNU in it in help output. Posix date does not. Use this to indicate on Linux systems without GNU date that the extended parsing is not @@ -63,7 +64,7 @@ def has_gnu_date(): return "GNU" in subp.subp(["date", "--help"]).stdout -def parse_timestamp_from_date(timestampstr): +def parse_timestamp_from_date(timestampstr: str) -> float: if not util.is_Linux() and subp.which("gdate"): date = "gdate" elif has_gnu_date(): @@ -77,7 +78,7 @@ def parse_timestamp_from_date(timestampstr): ) -def parse_ci_logline(line): +def parse_ci_logline(line: str) -> Optional[Dict[str, Any]]: # Stage Starts: # Cloud-init v. 0.7.7 running 'init-local' at \ # Fri, 02 Sep 2016 19:28:07 +0000. Up 1.0 seconds. @@ -163,7 +164,10 @@ def parse_ci_logline(line): return event -def dump_events(cisource=None, rawdata=None): +def dump_events( + cisource: Optional[IO[str]] = None, + rawdata: Optional[str] = None, +) -> Tuple[List[Dict[str, Any]], List[str]]: events = [] event = None CI_EVENT_MATCHES = ["start:", "finish:", "Cloud-init v."] @@ -171,9 +175,9 @@ def dump_events(cisource=None, rawdata=None): if not any([cisource, rawdata]): raise ValueError("Either cisource or rawdata parameters are required") - if rawdata: + if rawdata is not None: data = rawdata.splitlines() - else: + elif cisource is not None: data = cisource.readlines() for line in data: @@ -189,9 +193,9 @@ def dump_events(cisource=None, rawdata=None): return events, data -def main(): +def main() -> str: if len(sys.argv) > 1: - cisource = open(sys.argv[1]) + cisource: TextIO = open(sys.argv[1]) else: cisource = sys.stdin diff --git a/pyproject.toml b/pyproject.toml index 0d473ba9020..143154779e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ no_implicit_optional = true # See GH-5445 [[tool.mypy.overrides]] module = [ - "cloudinit.analyze.dump", "cloudinit.analyze.show", "cloudinit.cmd.devel.make_mime", "cloudinit.cmd.devel.net_convert", From deb0f9e5b739ad395482b99d5516af74a547dc7e Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Mon, 4 Aug 2025 22:38:05 +0200 Subject: [PATCH 052/150] fix(ENI): render keys with hyphens (#6333) In ENI spaces within keys can be represented as hyphens or underscores. Standardize on hyphens. Fixes GH-5234 Signed-off-by: Alberto Contreras --- cloudinit/net/eni.py | 7 +- tests/unittests/net/network_configs.py | 110 ++++++++++++------------- 2 files changed, 60 insertions(+), 57 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 43ace88777d..b363acbb392 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -129,6 +129,9 @@ def _iface_add_attrs( ignore_map.append("mac_address") for key, value in iface.items(): + key_write = renames.get(key, key) + if "_" in key_write: + key_write = key_write.replace("_", "-") # convert bool to string for eni if isinstance(value, bool): value = "on" if iface[key] else "off" @@ -146,11 +149,11 @@ def _iface_add_attrs( continue if key in multiline_keys: for v in value: - content.append(" {0} {1}".format(renames.get(key, key), v)) + content.append(" {0} {1}".format(key_write, v)) continue if isinstance(value, list): value = " ".join(value) - content.append(" {0} {1}".format(renames.get(key, key), value)) + content.append(" {0} {1}".format(key_write, value)) return sorted(content) diff --git a/tests/unittests/net/network_configs.py b/tests/unittests/net/network_configs.py index 2fdfc73baaf..fab4e3197fc 100644 --- a/tests/unittests/net/network_configs.py +++ b/tests/unittests/net/network_configs.py @@ -937,7 +937,7 @@ auto iface0 iface iface0 inet6 dhcp - accept_ra 1 + accept-ra 1 """ ).rstrip(" "), "expected_netplan": textwrap.dedent( @@ -1012,7 +1012,7 @@ auto iface0 iface iface0 inet6 dhcp - accept_ra 0 + accept-ra 0 """ ).rstrip(" "), "expected_netplan": textwrap.dedent( @@ -1509,16 +1509,16 @@ auto eth1 iface eth1 inet manual bond-master bond0 + bond-miimon 100 bond-mode active-backup bond-xmit-hash-policy layer3+4 - bond_miimon 100 auto eth2 iface eth2 inet manual bond-master bond0 + bond-miimon 100 bond-mode active-backup bond-xmit-hash-policy layer3+4 - bond_miimon 100 iface eth3 inet manual @@ -1535,29 +1535,29 @@ auto bond0 iface bond0 inet6 dhcp + bond-miimon 100 bond-mode active-backup bond-slaves none bond-xmit-hash-policy layer3+4 - bond_miimon 100 hwaddress aa:bb:cc:dd:ee:ff auto br0 iface br0 inet static address 192.168.14.2/24 - bridge_ageing 250 - bridge_bridgeprio 22 - bridge_fd 1 - bridge_gcint 2 - bridge_hello 1 - bridge_maxage 10 - bridge_pathcost eth3 50 - bridge_pathcost eth4 75 - bridge_portprio eth3 28 - bridge_portprio eth4 14 - bridge_ports eth3 eth4 - bridge_stp off - bridge_waitport 1 eth3 - bridge_waitport 2 eth4 + bridge-ageing 250 + bridge-bridgeprio 22 + bridge-fd 1 + bridge-gcint 2 + bridge-hello 1 + bridge-maxage 10 + bridge-pathcost eth3 50 + bridge-pathcost eth4 75 + bridge-portprio eth3 28 + bridge-portprio eth4 14 + bridge-ports eth3 eth4 + bridge-stp off + bridge-waitport 1 eth3 + bridge-waitport 2 eth4 hwaddress bb:bb:bb:bb:bb:aa # control-alias br0 @@ -1568,8 +1568,8 @@ auto bond0.200 iface bond0.200 inet dhcp + vlan-id 200 vlan-raw-device bond0 - vlan_id 200 auto eth0.101 iface eth0.101 inet static @@ -1579,8 +1579,8 @@ gateway 192.168.0.1 mtu 1500 hwaddress aa:bb:cc:dd:ee:11 + vlan-id 101 vlan-raw-device eth0 - vlan_id 101 # control-alias eth0.101 iface eth0.101 inet static @@ -2350,16 +2350,16 @@ auto eth1 iface eth1 inet manual bond-master bond0 + bond-miimon 100 bond-mode active-backup bond-xmit-hash-policy layer3+4 - bond_miimon 100 auto eth2 iface eth2 inet manual bond-master bond0 + bond-miimon 100 bond-mode active-backup bond-xmit-hash-policy layer3+4 - bond_miimon 100 iface eth3 inet manual @@ -2376,29 +2376,29 @@ auto bond0 iface bond0 inet6 dhcp + bond-miimon 100 bond-mode active-backup bond-slaves none bond-xmit-hash-policy layer3+4 - bond_miimon 100 hwaddress aa:bb:cc:dd:ee:ff auto br0 iface br0 inet static address 192.168.14.2/24 - bridge_ageing 250 - bridge_bridgeprio 22 - bridge_fd 1 - bridge_gcint 2 - bridge_hello 1 - bridge_maxage 10 - bridge_pathcost eth3 50 - bridge_pathcost eth4 75 - bridge_portprio eth3 28 - bridge_portprio eth4 14 - bridge_ports eth3 eth4 - bridge_stp off - bridge_waitport 1 eth3 - bridge_waitport 2 eth4 + bridge-ageing 250 + bridge-bridgeprio 22 + bridge-fd 1 + bridge-gcint 2 + bridge-hello 1 + bridge-maxage 10 + bridge-pathcost eth3 50 + bridge-pathcost eth4 75 + bridge-portprio eth3 28 + bridge-portprio eth4 14 + bridge-ports eth3 eth4 + bridge-stp off + bridge-waitport 1 eth3 + bridge-waitport 2 eth4 hwaddress bb:bb:bb:bb:bb:aa # control-alias br0 @@ -2409,8 +2409,8 @@ auto bond0.200 iface bond0.200 inet dhcp + vlan-id 200 vlan-raw-device bond0 - vlan_id 200 auto eth0.101 iface eth0.101 inet static @@ -2420,8 +2420,8 @@ gateway 192.168.0.1 mtu 1500 hwaddress aa:bb:cc:dd:ee:11 + vlan-id 101 vlan-raw-device eth0 - vlan_id 101 # control-alias eth0.101 iface eth0.101 inet static @@ -3138,43 +3138,43 @@ auto bond0s0 iface bond0s0 inet manual bond-downdelay 10 - bond-fail_over_mac active + bond-fail-over-mac active bond-master bond0 + bond-miimon 100 bond-mode active-backup - bond-num_grat_arp 5 + bond-num-grat-arp 5 bond-primary bond0s0 - bond-primary_reselect always + bond-primary-reselect always bond-updelay 20 - bond-xmit_hash_policy layer3+4 - bond_miimon 100 + bond-xmit-hash-policy layer3+4 auto bond0s1 iface bond0s1 inet manual bond-downdelay 10 - bond-fail_over_mac active + bond-fail-over-mac active bond-master bond0 + bond-miimon 100 bond-mode active-backup - bond-num_grat_arp 5 + bond-num-grat-arp 5 bond-primary bond0s0 - bond-primary_reselect always + bond-primary-reselect always bond-updelay 20 - bond-xmit_hash_policy layer3+4 - bond_miimon 100 + bond-xmit-hash-policy layer3+4 auto bond0 iface bond0 inet static address 192.168.0.2/24 gateway 192.168.0.1 bond-downdelay 10 - bond-fail_over_mac active + bond-fail-over-mac active + bond-miimon 100 bond-mode active-backup - bond-num_grat_arp 5 + bond-num-grat-arp 5 bond-primary bond0s0 - bond-primary_reselect always + bond-primary-reselect always bond-slaves none bond-updelay 20 - bond-xmit_hash_policy layer3+4 - bond_miimon 100 + bond-xmit-hash-policy layer3+4 hwaddress aa:bb:cc:dd:e8:ff mtu 9000 post-up route add -net 10.1.3.0/24 gw 192.168.0.3 || true From 13891e06a3b598ea4ef93b03770e15cc5889539b Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Thu, 10 Jul 2025 15:51:00 +0200 Subject: [PATCH 053/150] disk_setup: Fully support full GPT partition GUIDs disk_setup module partially support full GPT partition GUIDs already, i.e. it is possible to do something like: disk_setup: /dev/sdc: table_type: gpt layout: [[100, '44479540-F297-41B2-9AF7-D131D5F0458A']] but matching with the existing partition layout doesn't really work as 'sgdisk' returns its own, invented partition types (4 digit), e.g. '8303'. Extract gdisk-id-to-partition-type-GUID table from gdisk and introduce a simple partition_type_matches() which does a somewhat smarter comparison. Add a few unittests to cover the new functionallity. Signed-off-by: Vitaly Kuznetsov --- cloudinit/config/cc_disk_setup.py | 425 ++++++++++++++++++- doc/examples/cloud-config-disk-setup.txt | 10 +- tests/unittests/config/test_cc_disk_setup.py | 34 ++ 3 files changed, 456 insertions(+), 13 deletions(-) diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py index 0f3bf12d196..a17cf513b0d 100644 --- a/cloudinit/config/cc_disk_setup.py +++ b/cloudinit/config/cc_disk_setup.py @@ -396,6 +396,377 @@ def check_partition_mbr_layout(device, layout): return found_layout +# gdisk uses its own ids, convert them to standard GPT partition GUIDs. +# From gdisk sources: +# grep " AddType" parttypes.cc | +# sed -e 's,AddType(,,' -e 's,"\,.*$,"\,,' -e 's,0x,",' -e 's,\,,":,' | +# tr "[a-z]" "[A-Z]" +sgdisk_to_gpt_id = { + "0000": "00000000-0000-0000-0000-000000000000", + "0100": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "0400": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "0600": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "0700": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "0701": "558D43C5-A1AC-43C0-AAC8-D1472B2923D1", + "0702": "90B6FF38-B98F-4358-A21F-48F35B4A8AD3", + "0B00": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "0C00": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "0C01": "E3C9E316-0B5C-4DB8-817D-F92DF00215AE", + "0E00": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1100": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1400": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1600": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1700": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1B00": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1C00": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "1E00": "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + "2700": "DE94BBA4-06D1-4D40-A16A-BFD50179D6AC", + "3000": "7412F7D5-A156-4B13-81DC-867174929325", + "3001": "D4E6E2CD-4469-46F3-B5CB-1BFF57AFC149", + "3900": "C91818F9-8025-47AF-89D2-F030D7000C2C", + "4100": "9E1A2D38-C612-4316-AA26-8B49521E5A8B", + "4200": "AF9B60A0-1431-4F62-BC68-3311714A69AD", + "4201": "5808C8AA-7E8F-42E0-85D2-E1E90434CFB3", + "4202": "E75CAF8F-F680-4CEE-AFA3-B001E56EFC2D", + "7501": "37AFFC90-EF7D-4E96-91C3-2D7AE055B174", + "7F00": "FE3A2A5D-4F32-41A7-B725-ACCC3285A309", + "7F01": "3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC", + "7F02": "2E0A753D-9E48-43B0-8337-B15192CB1B5E", + "7F03": "CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3", + "7F04": "09845860-705F-4BB5-B16C-8A8A099CAF52", + "7F05": "3F0F8318-F146-4E6B-8222-C28C8F02E0D5", + "8200": "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", + "8300": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "8301": "8DA63339-0007-60C0-C436-083AC8230908", + "8302": "933AC7E1-2EB4-4F13-B844-0E14E2AEF915", + "8303": "44479540-F297-41B2-9AF7-D131D5F0458A", + "8304": "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", + "8305": "B921B045-1DF0-41C3-AF44-4C6F280D3FAE", + "8306": "3B8F8425-20E0-4F3B-907F-1A25A76F98E8", + "8307": "69DAD710-2CE4-4E3C-B16C-21A1D49ABED3", + "8308": "7FFEC5C9-2D00-49B7-8941-3EA10A5586B7", + "8309": "CA7D7CCB-63ED-4C53-861C-1742536059CC", + "830A": "993D8D3D-F80E-4225-855A-9DAF8ED7EA97", + "830B": "D13C5D3B-B5D1-422A-B29F-9454FDC89D76", + "830C": "2C7357ED-EBD2-46D9-AEC1-23D437EC2BF5", + "830D": "7386CDF2-203C-47A9-A498-F2ECCE45A2D6", + "830E": "DF3300CE-D69F-4C92-978C-9BFB0F38D820", + "830F": "86ED10D5-B607-45BB-8957-D350F23D0571", + "8310": "4D21B016-B534-45C2-A9FB-5C16E091FD2D", + "8311": "7EC6F557-3BC5-4ACA-B293-16EF5DF639D1", + "8312": "773F91EF-66D4-49B5-BD83-D683BF40AD16", + "8313": "75250D76-8CC6-458E-BD66-BD47CC81A812", + "8314": "8484680C-9521-48C6-9C11-B0720656F69E", + "8315": "7D0359A3-02B3-4F0A-865C-654403E70625", + "8316": "B0E01050-EE5F-4390-949A-9101B17104E9", + "8317": "4301D2A6-4E3B-4B2A-BB94-9E0B2C4225EA", + "8318": "8F461B0D-14EE-4E81-9AA9-049B6FB97ABD", + "8319": "77FF5F63-E7B6-4633-ACF4-1565B864C0E6", + "831A": "C215D751-7BCD-4649-BE90-6627490A4C05", + "831B": "6E11A4E7-FBCA-4DED-B9E9-E1A512BB664E", + "831C": "6A491E03-3BE7-4545-8E38-83320E0EA880", + "831D": "6523F8AE-3EB1-4E2A-A05A-18B695AE656F", + "831E": "D27F46ED-2919-4CB8-BD25-9531F3C16534", + "831F": "77055800-792C-4F94-B39A-98C91B762BB6", + "8320": "E9434544-6E2C-47CC-BAE2-12D6DEAFB44C", + "8321": "D113AF76-80EF-41B4-BDB6-0CFF4D3D4A25", + "8322": "37C58C8A-D913-4156-A25F-48B1B64E07F0", + "8323": "700BDA43-7A34-4507-B179-EEB93D7A7CA3", + "8324": "1AACDB3B-5444-4138-BD9E-E5C2239B2346", + "8325": "1DE3F1EF-FA98-47B5-8DCD-4A860A654D78", + "8326": "912ADE1D-A839-4913-8964-A10EEE08FBD2", + "8327": "C31C45E6-3F39-412E-80FB-4809C4980599", + "8328": "60D5A7FE-8E7D-435C-B714-3DD8162144E1", + "8329": "72EC70A6-CF74-40E6-BD49-4BDA08E8F224", + "832A": "08A7ACEA-624C-4A20-91E8-6E0FA67D23F9", + "832B": "5EEAD9A9-FE09-4A1E-A1D7-520D00531306", + "832C": "C50CDD70-3862-4CC3-90E1-809A8C93EE2C", + "832D": "E18CF08C-33EC-4C0D-8246-C6C6FB3DA024", + "832E": "7978A683-6316-4922-BBEE-38BFF5A2FECC", + "832F": "E611C702-575C-4CBE-9A46-434FA0BF7E3F", + "8330": "773B2ABC-2A99-4398-8BF5-03BAAC40D02B", + "8331": "57E13958-7331-4365-8E6E-35EEEE17C61B", + "8332": "0F4868E9-9952-4706-979F-3ED3A473E947", + "8333": "C97C1F32-BA06-40B4-9F22-236061B08AA8", + "8334": "DC4A4480-6917-4262-A4EC-DB9384949F25", + "8335": "7D14FEC5-CC71-415D-9D6C-06BF0B3C3EAF", + "8336": "2C9739E2-F068-46B3-9FD0-01C5A9AFBCCA", + "8337": "15BB03AF-77E7-4D4A-B12B-C0D084F7491C", + "8338": "B933FB22-5C3F-4F91-AF90-E2BB0FA50702", + "8339": "BEAEC34B-8442-439B-A40B-984381ED097D", + "833A": "CD0F869B-D0FB-4CA0-B141-9EA87CC78D66", + "833B": "8A4F5770-50AA-4ED3-874A-99B710DB6FEA", + "833C": "55497029-C7C1-44CC-AA39-815ED1558630", + "833D": "FC56D9E9-E6E5-4C06-BE32-E74407CE09A5", + "833E": "24B2D975-0F97-4521-AFA1-CD531E421B8D", + "833F": "F3393B22-E9AF-4613-A948-9D3BFBD0C535", + "8340": "7A430799-F711-4C7E-8E5B-1D685BD48607", + "8341": "579536F8-6A33-4055-A95A-DF2D5E2C42A8", + "8342": "D7D150D2-2A04-4A33-8F12-16651205FF7B", + "8343": "16B417F8-3E06-4F57-8DD2-9B5232F41AA6", + "8344": "D212A430-FBC5-49F9-A983-A7FEEF2B8D0E", + "8345": "906BD944-4589-4AAE-A4E4-DD983917446A", + "8346": "9225A9A3-3C19-4D89-B4F6-EEFF88F17631", + "8347": "98CFE649-1588-46DC-B2F0-ADD147424925", + "8348": "AE0253BE-1167-4007-AC68-43926C14C5DE", + "8349": "B6ED5582-440B-4209-B8DA-5FF7C419EA3D", + "834A": "7AC63B47-B25C-463B-8DF8-B4A94E6C90E1", + "834B": "B325BFBE-C7BE-4AB8-8357-139E652D2F6B", + "834C": "966061EC-28E4-4B2E-B4A5-1F0A825A1D84", + "834D": "8CCE0D25-C0D0-4A44-BD87-46331BF1DF67", + "834E": "FCA0598C-D880-4591-8C16-4EDA05C7347C", + "834F": "F46B2C26-59AE-48F0-9106-C50ED47F673D", + "8350": "6E5A1BC8-D223-49B7-BCA8-37A5FCCEB996", + "8351": "81CF9D90-7458-4DF4-8DCF-C8A3A404F09B", + "8352": "46B98D8D-B55C-4E8F-AAB3-37FCA7F80752", + "8353": "3C3D61FE-B5F3-414D-BB71-8739A694A4EF", + "8354": "5843D618-EC37-48D7-9F12-CEA8E08768B2", + "8355": "EE2B9983-21E8-4153-86D9-B6901A54D1CE", + "8356": "BDB528A5-A259-475F-A87D-DA53FA736A07", + "8357": "DF765D00-270E-49E5-BC75-F47BB2118B09", + "8358": "CB1EE4E3-8CD0-4136-A0A4-AA61A32E8730", + "8359": "8F1056BE-9B05-47C4-81D6-BE53128E5B54", + "835A": "B663C618-E7BC-4D6D-90AA-11B756BB1797", + "835B": "31741CC4-1A2A-4111-A581-E00B447D2D06", + "835C": "2FB4BF56-07FA-42DA-8132-6B139F2026AE", + "835D": "D46495B7-A053-414F-80F7-700C99921EF8", + "835E": "143A70BA-CBD3-4F06-919F-6C05683A78BC", + "835F": "42B0455F-EB11-491D-98D3-56145BA9D037", + "8360": "6DB69DE6-29F4-4758-A7A5-962190F00CE3", + "8361": "E98B36EE-32BA-4882-9B12-0CE14655F46A", + "8362": "5AFB67EB-ECC8-4F85-AE8E-AC1E7C50E7D0", + "8363": "BBA210A2-9C5D-45EE-9E87-FF2CCBD002D0", + "8364": "43CE94D4-0F3D-4999-8250-B9DEAFD98E6E", + "8365": "C919CC1F-4456-4EFF-918C-F75E94525CA5", + "8366": "904E58EF-5C65-4A31-9C57-6AF5FC7C5DE7", + "8367": "15DE6170-65D3-431C-916E-B0DCD8393F25", + "8368": "D4A236E7-E873-4C07-BF1D-BF6CF7F1C3C6", + "8369": "F5E2C20C-45B2-4FFA-BCE9-2A60737E1AAF", + "836A": "1B31B5AA-ADD9-463A-B2ED-BD467FC857E7", + "836B": "3A112A75-8729-4380-B4CF-764D79934448", + "836C": "EFE0F087-EA8D-4469-821A-4C2A96A8386A", + "836D": "3482388E-4254-435A-A241-766A065F9960", + "836E": "C80187A5-73A3-491A-901A-017C3FA953E9", + "836F": "B3671439-97B0-4A53-90F7-2D5A8F3AD47B", + "8370": "41092B05-9FC8-4523-994F-2DEF0408B176", + "8371": "5996FC05-109C-48DE-808B-23FA0830B676", + "8372": "5C6E1C76-076A-457A-A0FE-F3B4CD21CE6E", + "8373": "94F9A9A1-9971-427A-A400-50CB297F0F35", + "8374": "D7FF812F-37D1-4902-A810-D76BA57B975A", + "8375": "C23CE4FF-44BD-4B00-B2D4-B41B3419E02A", + "8376": "8DE58BC2-2A43-460D-B14E-A76E4A17B47F", + "8377": "B024F315-D330-444C-8461-44BBDE524E99", + "8378": "97AE158D-F216-497B-8057-F7F905770F54", + "8379": "05816CE2-DD40-4AC6-A61D-37D32DC1BA7D", + "837A": "3E23CA0B-A4BC-4B4E-8087-5AB6A26AA8A9", + "837B": "F2C2C7EE-ADCC-4351-B5C6-EE9816B66E16", + "837C": "450DD7D1-3224-45EC-9CF2-A43A346D71EE", + "837D": "C8BFBD1E-268E-4521-8BBA-BF314C399557", + "837E": "0B888863-D7F8-4D9E-9766-239FCE4D58AF", + "837F": "7007891D-D371-4A80-86A4-5CB875B9302E", + "8380": "C3836A13-3137-45BA-B583-B16C50FE5EB4", + "8381": "D2F9000A-7A18-453F-B5CD-4D32F77A7B32", + "8382": "17440E4F-A8D0-467F-A46E-3912AE6EF2C5", + "8383": "3F324816-667B-46AE-86EE-9B0C0C6C11B4", + "8384": "4EDE75E2-6CCC-4CC8-B9C7-70334B087510", + "8385": "E7BB33FB-06CF-4E81-8273-E543B413E2E2", + "8386": "974A71C0-DE41-43C3-BE5D-5C5CCD1AD2C0", + "8400": "D3BFE2DE-3DAF-11DF-BA40-E3A556D89593", + "8401": "7C5222BD-8F5D-4087-9C00-BF9843C7B58C", + "8500": "5DFBF5F4-2848-4BAC-AA5E-0D9A20B745A6", + "8501": "3884DD41-8582-4404-B9A8-E9B84F2DF50E", + "8502": "C95DC21A-DF0E-4340-8D7B-26CBFA9A03E0", + "8503": "BE9067B9-EA49-4F15-B4F6-F36F8C9E1818", + "8E00": "E6D6D379-F507-44C2-A23C-238F2A3DF928", + "A000": "2568845D-2332-4675-BC39-8FA5A4748D15", + "A001": "114EAFFE-1552-4022-B26E-9B053604CF84", + "A002": "49A4D17F-93A3-45C1-A0DE-F50B2EBE2599", + "A003": "4177C722-9E92-4AAB-8644-43502BFD5506", + "A004": "EF32A33B-A409-486C-9141-9FFB711F6266", + "A005": "20AC26BE-20B7-11E3-84C5-6CFDB94711E9", + "A006": "38F428E6-D326-425D-9140-6E0EA133647C", + "A007": "A893EF21-E428-470A-9E55-0668FD91A2D9", + "A008": "DC76DDA9-5AC1-491C-AF42-A82591580C0D", + "A009": "EBC597D0-2053-4B15-8B64-E0AAC75F4DB1", + "A00A": "8F68CC74-C5E5-48DA-BE91-A0C8C15E9C80", + "A00B": "767941D0-2085-11E3-AD3B-6CFDB94711E9", + "A00C": "AC6D7924-EB71-4DF8-B48D-E267B27148FF", + "A00D": "C5A0AEEC-13EA-11E5-A1B1-001E67CA0C3C", + "A00E": "BD59408B-4514-490D-BF12-9878D963F378", + "A00F": "9FDAA6EF-4B3F-40D2-BA8D-BFF16BFB887B", + "A010": "19A710A2-B3CA-11E4-B026-10604B889DCF", + "A011": "193D1EA4-B3CA-11E4-B075-10604B889DCF", + "A012": "DEA0BA2C-CBDD-4805-B4F9-F428251C3E98", + "A013": "8C6B52AD-8A9E-4398-AD09-AE916E53AE2D", + "A014": "05E044DF-92F1-4325-B69E-374A82E97D6E", + "A015": "400FFDCD-22E0-47E7-9A23-F16ED9382388", + "A016": "A053AA7F-40B8-4B1C-BA08-2F68AC71A4F4", + "A017": "E1A6A689-0C8D-4CC6-B4E8-55A4320FBD8A", + "A018": "098DF793-D712-413D-9D4E-89D711772228", + "A019": "D4E0D938-B7FA-48C1-9D21-BC5ED5C4B203", + "A01A": "20A0C19C-286A-42FA-9CE7-F64C3226A794", + "A01B": "A19F205F-CCD8-4B6D-8F1E-2D9BC24CFFB1", + "A01C": "66C9B323-F7FC-48B6-BF96-6F32E335A428", + "A01D": "303E6AC3-AF15-4C54-9E9B-D9A8FBECF401", + "A01E": "C00EEF24-7709-43D6-9799-DD2B411E7A3C", + "A01F": "82ACC91F-357C-4A68-9C8F-689E1B1A23A1", + "A020": "E2802D54-0545-E8A1-A1E8-C7A3E245ACD4", + "A021": "65ADDCF4-0C5C-4D9A-AC2D-D90B5CBFCD03", + "A022": "E6E98DA2-E22A-4D12-AB33-169E7DEAA507", + "A023": "ED9E8101-05FA-46B7-82AA-8D58770D200B", + "A024": "11406F35-1173-4869-807B-27DF71802812", + "A025": "9D72D4E4-9958-42DA-AC26-BEA7A90B0434", + "A026": "6C95E238-E343-4BA8-B489-8681ED22AD0B", + "A027": "EBBEADAF-22C9-E33B-8F5D-0E81686A68CB", + "A028": "0A288B1F-22C9-E33B-8F5D-0E81686A68CB", + "A029": "57B90A16-22C9-E33B-8F5D-0E81686A68CB", + "A02A": "638FF8E2-22C9-E33B-8F5D-0E81686A68CB", + "A02B": "2013373E-1AC4-4131-BFD8-B6A7AC638772", + "A02C": "2C86E742-745E-4FDD-BFD8-B6A7AC638772", + "A02D": "DE7D4029-0F5B-41C8-AE7E-F6C023A02B33", + "A02E": "323EF595-AF7A-4AFA-8060-97BE72841BB9", + "A02F": "45864011-CF89-46E6-A445-85262E065604", + "A030": "8ED8AE95-597F-4C8A-A5BD-A7FF8E4DFAA9", + "A031": "DF24E5ED-8C96-4B86-B00B-79667DC6DE11", + "A032": "7C29D3AD-78B9-452E-9DEB-D098D542F092", + "A033": "379D107E-229E-499D-AD4F-61F5BCF87BD4", + "A034": "0DEA65E5-A676-4CDF-823C-77568B577ED5", + "A035": "4627AE27-CFEF-48A1-88FE-99C3509ADE26", + "A036": "20117F86-E985-4357-B9EE-374BC1D8487D", + "A037": "86A7CB80-84E1-408C-99AB-694F1A410FC7", + "A038": "97D7B011-54DA-4835-B3C4-917AD6E73D74", + "A039": "5594C694-C871-4B5F-90B1-690A6F68E0F7", + "A03A": "1B81E7E6-F50D-419B-A739-2AEEF8DA3335", + "A03B": "98523EC6-90FE-4C67-B50A-0FC59ED6F56D", + "A03C": "2644BCC0-F36A-4792-9533-1738BED53EE3", + "A03D": "DD7C91E9-38C9-45C5-8A12-4A80F7E14057", + "A03E": "7696D5B6-43FD-4664-A228-C563C4A1E8CC", + "A03F": "0D802D54-058D-4A20-AD2D-C7A362CEACD4", + "A040": "10A0C19C-516A-5444-5CE3-664C3226A794", + "A200": "734E5AFE-F61A-11E6-BC64-92361F002671", + "A500": "516E7CB4-6ECF-11D6-8FF8-00022D09712B", + "A501": "83BD6B9D-7F41-11DC-BE0B-001560B84F0F", + "A502": "516E7CB5-6ECF-11D6-8FF8-00022D09712B", + "A503": "516E7CB6-6ECF-11D6-8FF8-00022D09712B", + "A504": "516E7CBA-6ECF-11D6-8FF8-00022D09712B", + "A505": "516E7CB8-6ECF-11D6-8FF8-00022D09712B", + "A506": "74BA7DD9-A689-11E1-BD04-00E081286ACF", + "A580": "85D5E45A-237C-11E1-B4B3-E89A8F7FC3A7", + "A581": "85D5E45E-237C-11E1-B4B3-E89A8F7FC3A7", + "A582": "85D5E45B-237C-11E1-B4B3-E89A8F7FC3A7", + "A583": "0394EF8B-237E-11E1-B4B3-E89A8F7FC3A7", + "A584": "85D5E45D-237C-11E1-B4B3-E89A8F7FC3A7", + "A585": "85D5E45C-237C-11E1-B4B3-E89A8F7FC3A7", + "A600": "824CC7A0-36A8-11E3-890A-952519AD3F61", + "A800": "55465300-0000-11AA-AA11-00306543ECAC", + "A900": "516E7CB4-6ECF-11D6-8FF8-00022D09712B", + "A901": "49F48D32-B10E-11DC-B99B-0019D1879648", + "A902": "49F48D5A-B10E-11DC-B99B-0019D1879648", + "A903": "49F48D82-B10E-11DC-B99B-0019D1879648", + "A904": "2DB519C4-B10F-11DC-B99B-0019D1879648", + "A905": "2DB519EC-B10F-11DC-B99B-0019D1879648", + "A906": "49F48DAA-B10E-11DC-B99B-0019D1879648", + "AB00": "426F6F74-0000-11AA-AA11-00306543ECAC", + "AF00": "48465300-0000-11AA-AA11-00306543ECAC", + "AF01": "52414944-0000-11AA-AA11-00306543ECAC", + "AF02": "52414944-5F4F-11AA-AA11-00306543ECAC", + "AF03": "4C616265-6C00-11AA-AA11-00306543ECAC", + "AF04": "5265636F-7665-11AA-AA11-00306543ECAC", + "AF05": "53746F72-6167-11AA-AA11-00306543ECAC", + "AF06": "B6FA30DA-92D2-4A9A-96F1-871EC6486200", + "AF07": "2E313465-19B9-463F-8126-8A7993773801", + "AF08": "FA709C7E-65B1-4593-BFD5-E71D61DE9B02", + "AF09": "BBBA6DF5-F46F-4A89-8F59-8765B2727503", + "AF0A": "7C3457EF-0000-11AA-AA11-00306543ECAC", + "AF0B": "69646961-6700-11AA-AA11-00306543ECAC", + "AF0C": "52637672-7900-11AA-AA11-00306543ECAC", + "B000": "3DE21764-95BD-54BD-A5C3-4ABE786F38A8", + "B300": "CEF5A9AD-73BC-4601-89F3-CDEEEEE321A1", + "BB00": "4778ED65-BF42-45FA-9C5B-287A1DC4AAB1", + "BC00": "0311FC50-01CA-4725-AD77-9ADBB20ACE98", + "BE00": "6A82CB45-1DD2-11B2-99A6-080020736631", + "BF00": "6A85CF4D-1DD2-11B2-99A6-080020736631", + "BF01": "6A898CC3-1DD2-11B2-99A6-080020736631", + "BF02": "6A87C46F-1DD2-11B2-99A6-080020736631", + "BF03": "6A8B642B-1DD2-11B2-99A6-080020736631", + "BF04": "6A8EF2E9-1DD2-11B2-99A6-080020736631", + "BF05": "6A90BA39-1DD2-11B2-99A6-080020736631", + "BF06": "6A9283A5-1DD2-11B2-99A6-080020736631", + "BF07": "6A945A3B-1DD2-11B2-99A6-080020736631", + "BF08": "6A9630D1-1DD2-11B2-99A6-080020736631", + "BF09": "6A980767-1DD2-11B2-99A6-080020736631", + "BF0A": "6A96237F-1DD2-11B2-99A6-080020736631", + "BF0B": "6A8D2AC7-1DD2-11B2-99A6-080020736631", + "C001": "75894C1E-3AEB-11D3-B7C1-7B03A0000000", + "C002": "E2A1E728-32E3-11D6-A682-7B03A0000000", + "E100": "7412F7D5-A156-4B13-81DC-867174929325", + "E101": "D4E6E2CD-4469-46F3-B5CB-1BFF57AFC149", + "E900": "8C8F8EFF-AC95-4770-814A-21994F2DBC8F", + "EA00": "BC13C2FF-59E6-4262-A352-B275FD6F7172", + "EB00": "42465331-3BA3-10F1-802A-4861696B7521", + "ED00": "F4019732-066E-4E12-8273-346C5641494F", + "ED01": "BFBFAFE7-A34F-448A-9A5B-6213EB736C22", + "EF00": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "EF01": "024DEE41-33E7-11D3-9D69-0008C781F39F", + "EF02": "21686148-6449-6E6F-744E-656564454649", + "F100": "FE8A2634-5E2E-46BA-99E3-3A192091A350", + "F101": "D9FD4535-106C-4CEC-8D37-DFC020CA87CB", + "F102": "A409E16B-78AA-4ACC-995C-302352621A41", + "F103": "F95D940E-CABA-4578-9B93-BB6C90F29D3E", + "F104": "10B8DBAA-D2BF-42A9-98C6-A7C5DB3701E7", + "F105": "49FD7CB8-DF15-4E73-B9D9-992070127F0F", + "F106": "421A8BFC-85D9-4D85-ACDA-B64EEC0133E9", + "F107": "9B37FFF6-2E58-466A-983A-F7926D0B04E0", + "F108": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "F109": "606B000B-B7C7-4653-A7D5-B737332C899D", + "F10A": "08185F0C-892D-428A-A789-DBEEC8F55E6A", + "F10B": "48435546-4953-2041-494E-5354414C4C52", + "F10C": "2967380E-134C-4CBB-B6DA-17E7CE1CA45D", + "F10D": "41D0E340-57E3-954E-8C1E-17ECAC44CFF5", + "F10E": "DE30CC86-1F4A-4A31-93C4-66F147D33E05", + "F10F": "23CC04DF-C278-4CE7-8471-897D1A4BCDF7", + "F110": "A0E5CF57-2DEF-46BE-A80C-A2067C37CD49", + "F111": "4E5E989E-4C86-11E8-A15B-480FCF35F8E6", + "F112": "5A3A90BE-4C86-11E8-A15B-480FCF35F8E6", + "F113": "5ECE94FE-4C86-11E8-A15B-480FCF35F8E6", + "F114": "8B94D043-30BE-4871-9DFA-D69556E8C1F3", + "F115": "A13B4D9A-EC5F-11E8-97D8-6C3BE52705BF", + "F116": "A288ABF2-EC5F-11E8-97D8-6C3BE52705BF", + "F117": "6A2460C3-CD11-4E8B-80A8-12CCE268ED0A", + "F118": "1D75395D-F2C6-476B-A8B7-45CC1C97B476", + "F119": "900B0FC5-90CD-4D4F-84F9-9F8ED579DB88", + "F11A": "B2B2E8D1-7C10-4EBC-A2D0-4614568260AD", + "F800": "4FBD7E29-9D25-41B8-AFD0-062C0CEFF05D", + "F801": "4FBD7E29-9D25-41B8-AFD0-5EC00CEFF05D", + "F802": "45B0969E-9B03-4F30-B4C6-B4B80CEFF106", + "F803": "45B0969E-9B03-4F30-B4C6-5EC00CEFF106", + "F804": "89C57F98-2FE5-4DC0-89C1-F3AD0CEFF2BE", + "F805": "89C57F98-2FE5-4DC0-89C1-5EC00CEFF2BE", + "F806": "CAFECAFE-9B03-4F30-B4C6-B4B80CEFF106", + "F807": "30CD0809-C2B2-499C-8879-2D6B78529876", + "F808": "5CE17FCE-4087-4169-B7FF-056CC58473F9", + "F809": "FB3AABF9-D25F-47CC-BF5E-721D1816496B", + "F80A": "4FBD7E29-8AE0-4982-BF9D-5A8D867AF560", + "F80B": "45B0969E-8AE0-4982-BF9D-5A8D867AF560", + "F80C": "CAFECAFE-8AE0-4982-BF9D-5A8D867AF560", + "F80D": "7F4A666A-16F3-47A2-8445-152EF4D03F6C", + "F80E": "EC6D6385-E346-45DC-BE91-DA2A7C8B3261", + "F80F": "01B41E1B-002A-453C-9F17-88793989FF8F", + "F810": "CAFECAFE-9B03-4F30-B4C6-5EC00CEFF106", + "F811": "93B0052D-02D9-4D8A-A43B-33A3EE4DFBC3", + "F812": "306E8683-4FE2-4330-B7C0-00A917C16966", + "F813": "45B0969E-9B03-4F30-B4C6-35865CEFF106", + "F814": "CAFECAFE-9B03-4F30-B4C6-35865CEFF106", + "F815": "166418DA-C469-4022-ADF4-B30AFD37F176", + "F816": "86A32090-3647-40B9-BBBD-38D8C573AA86", + "F817": "4FBD7E29-9D25-41B8-AFD0-35865CEFF05D", + "FB00": "AA31E02A-400F-11DB-9590-000C2911D1B8", + "FB01": "9198EFFC-31C0-11DB-8F78-000C2911D1B8", + "FC00": "9D275380-40AD-11DB-BF97-000C2911D1B8", + "FD00": "A19D880F-05FC-4D3B-A006-743F0F84911E", +} + + def check_partition_gpt_layout(device, layout): prt_cmd = ["sgdisk", "-p", device] try: @@ -426,15 +797,51 @@ def check_partition_gpt_layout(device, layout): if line.strip().startswith("Number"): break - codes = [line.strip().split()[5] for line in out_lines] - cleaned = [] + return [line.strip().split()[5] for line in out_lines] + + +def partition_type_matches(found_type, expected_type): + """ + Check if the observed partition type matches the expectation which + can either be a two digit legacy code, a four digit 'sgdisk' type or + a GPT UUID. + """ + found_type = str(found_type).upper() + if len(found_type) not in [2, 4, 36]: + raise RuntimeError("Unknown partition type found: %s" % found_type) + + expected_type = str(expected_type).upper() + if len(expected_type) not in [2, 4, 36]: + raise RuntimeError("Unknown partition type specified: %s" % found_type) + + # Promote 2-digit codes to 4-digit + if len(found_type) == 2: + found_type += "00" + if len(expected_type) == 2: + expected_type += "00" + + # Check if four digit codes match + if len(found_type) == len(expected_type): + return found_type == expected_type + + # Promote four digit codes to GPT partition GUIDs + if len(found_type) == 4: + if found_type in sgdisk_to_gpt_id: + found_type = sgdisk_to_gpt_id[found_type] + else: + raise RuntimeError( + "Cannot find GPT GUID for found type %s" % found_type + ) + if len(expected_type) == 4: + if expected_type in sgdisk_to_gpt_id: + expected_type = sgdisk_to_gpt_id[expected_type] + else: + raise RuntimeError( + "Cannot find GPT GUID for expected type %s" % found_type + ) - # user would expect a code '83' to be Linux, but sgdisk outputs 8300. - for code in codes: - if len(code) == 4 and code.endswith("00"): - code = code[0:2] - cleaned.append(code) - return cleaned + # Both codes are GPT UUIDs now + return found_type == expected_type def check_partition_layout(table_type, device, layout): @@ -476,7 +883,7 @@ def check_partition_layout(table_type, device, layout): "Layout types=%s. Found types=%s", layout_types, found_layout ) for itype, ftype in zip(layout_types, found_layout): - if itype is not None and str(ftype) != str(itype): + if itype is not None and not partition_type_matches(itype, ftype): return False return True diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt index 3c8fc36c507..cbdf0733e25 100644 --- a/doc/examples/cloud-config-disk-setup.txt +++ b/doc/examples/cloud-config-disk-setup.txt @@ -82,11 +82,11 @@ disk_setup: table_type: 'mbr' layout: true /dev/xvdh: - table_type: 'mbr' + table_type: 'gpt' layout: - 33 - [33, 82] - - 33 + - [33, '44479540-F297-41B2-9AF7-D131D5F0458A'] overwrite: True # The format is a list of dicts of dicts. The first value is the name of the @@ -95,7 +95,7 @@ disk_setup: # The general format is: # disk_setup: # : -# table_type: 'mbr' +# table_type: 'mbr'|'gpt' # layout: # overwrite: # @@ -126,7 +126,9 @@ disk_setup: # [, [, is the _percentage_ of the disk to use, while -# is the numerical value of the partition type. +# is either the numerical value of the partition type +# (two digit code from fdisk, four digit code from gdisk) or a full +# GPT partition GUID. # # The following setups two partitions, with the first # partition having a swap label, taking 1/3 of the disk space diff --git a/tests/unittests/config/test_cc_disk_setup.py b/tests/unittests/config/test_cc_disk_setup.py index 296c15d47af..b81a3a97e92 100644 --- a/tests/unittests/config/test_cc_disk_setup.py +++ b/tests/unittests/config/test_cc_disk_setup.py @@ -104,6 +104,40 @@ def test_thirds_with_different_partition_type(self): ) == cc_disk_setup.get_partition_mbr_layout(disk_size, [33, [66, 82]]) +class TestCheckPartitionLayout: + @mock.patch( + "cloudinit.config.cc_disk_setup.check_partition_mbr_layout", + return_value=["83"], + ) + def test_simple_mbr(self, *args): + assert cc_disk_setup.check_partition_layout("mbr", "/dev/xvdb1", True) + assert cc_disk_setup.check_partition_layout( + "mbr", "/dev/xvdb1", [(100, 83)] + ) + + @mock.patch( + "cloudinit.config.cc_disk_setup.check_partition_gpt_layout", + return_value=["8300"], + ) + def test_simple1_gpt(self, *args): + assert cc_disk_setup.check_partition_layout( + "gpt", "/dev/xvdb1", [(100, 83)] + ) + assert cc_disk_setup.check_partition_layout( + "gpt", "/dev/xvdb1", [(100, 8300)] + ) + assert ( + cc_disk_setup.check_partition_layout( + "gpt", "/dev/xvdb1", [(100, 8301)] + ) + is False + ) + Linux_GUID = "0FC63DAF-8483-4772-8E79-3D69D8477DE4" + assert cc_disk_setup.check_partition_layout( + "gpt", "/dev/xvdb1", [(100, Linux_GUID)] + ) + + class TestUpdateFsSetupDevices: def test_regression_1634678(self): # Cf. https://bugs.launchpad.net/cloud-init/+bug/1634678 From e210598d93c1e8005d9427f0bcdc080f5c32b4d7 Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Thu, 10 Jul 2025 16:50:38 +0200 Subject: [PATCH 054/150] disk_setup: Optionally use 'sfdisk' for GPT 'fdisk' from util-linux supports GPT from 2.23 (2012) and some distros don't ship superfluous 'gdisk' utility anymore. In particular, CentosStream-10/ RHEL10 don't have it and carry a downstream-only cloud-init patch to use 'sfdisk' instead of 'sgdisk'. Previously, an attempt was made to get rid of 'gdisk' dependency altogether: https://github.com/canonical/cloud-init/pull/5278 but the concern is that at least FreeBSD's sfdisk doesn't yet support GPT. So keep the status quo for now: use 'sgdisk' when available and try 'sfdisk' for GPT otherwise. The main difference between 'sgdisk' and 'sfdisk' is that the former uses its own, 4 digit partition type ids while the later requires standard GPT partition type GUIDs. 'sgdisk_to_gpt_id' is used to make the conversion. When checking partition layout with 'sfdisk', use its JSON output as types like 'Linux root (x86)' are observed otherwise. This can, in theory, be used for MBR case too but it's unclear whether it is supported everywhere (e.g. on BSD) so pick a safer path and only use it for GPT-without-sgdisk case. Fixes GH-5797 Signed-off-by: Vitaly Kuznetsov --- cloudinit/config/cc_disk_setup.py | 62 ++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py index a17cf513b0d..058d27ea97e 100644 --- a/cloudinit/config/cc_disk_setup.py +++ b/cloudinit/config/cc_disk_setup.py @@ -7,6 +7,7 @@ """Disk Setup: Configure partitions and filesystems.""" +import json import logging import os import shlex @@ -767,7 +768,7 @@ def check_partition_mbr_layout(device, layout): } -def check_partition_gpt_layout(device, layout): +def check_partition_gpt_layout_sgdisk(device, layout): prt_cmd = ["sgdisk", "-p", device] try: out, _err = subp.subp(prt_cmd, update_env=LANG_C_ENV) @@ -800,6 +801,31 @@ def check_partition_gpt_layout(device, layout): return [line.strip().split()[5] for line in out_lines] +def check_partition_gpt_layout_sfdisk(device, layout): + # Use sfdisk's JSON output for reliability + prt_cmd = ["sfdisk", "-l", "-J", device] + try: + out, _err = subp.subp(prt_cmd, update_env=LANG_C_ENV) + ptable = json.loads(out)["partitiontable"] + if "partitions" in ptable: + partitions = ptable["partitions"] + else: + partitions = [] + + except Exception as e: + raise RuntimeError( + "Error running partition command on %s\n%s" % (device, e) + ) from e + + return [p["type"] for p in partitions] + + +def check_partition_gpt_layout(device, layout): + if subp.which("sgdisk"): + return check_partition_gpt_layout_sgdisk(device, layout) + return check_partition_gpt_layout_sfdisk(device, layout) + + def partition_type_matches(found_type, expected_type): """ Check if the observed partition type matches the expectation which @@ -1058,7 +1084,7 @@ def exec_mkpart_mbr(device, layout): read_parttbl(device) -def exec_mkpart_gpt(device, layout): +def exec_mkpart_gpt_sgdisk(device, layout): try: subp.subp(["sgdisk", "-Z", device]) for index, (partition_type, (start, end)) in enumerate(layout): @@ -1082,6 +1108,38 @@ def exec_mkpart_gpt(device, layout): LOG.warning("Failed to partition device %s", device) raise + +def exec_mkpart_gpt_sfdisk(device, layout): + cmd = "" + # Promote partition types to GPT partition GUIDs + for partition_type, (start, end) in layout: + partition_type = str(partition_type).ljust(4, "0") + if len(partition_type) == 4 and partition_type in sgdisk_to_gpt_id: + partition_type = sgdisk_to_gpt_id[partition_type] + if len(partition_type) != 36: + if partition_type != "None": + LOG.warning( + "Unknown GPT partition type %s, using Linux", + partition_type, + ) + partition_type = "0FC63DAF-8483-4772-8E79-3D69D8477DE4" + if str(end) != "0": + cmd += ",%s,%s\n" % (end, partition_type) + else: + cmd += ",,%s\n" % partition_type + try: + subp.subp(["sfdisk", "-X", "gpt", "--force", device], data="%s" % cmd) + except Exception: + LOG.warning("Failed to partition device %s", device) + raise + + +def exec_mkpart_gpt(device, layout): + if subp.which("sgdisk"): + exec_mkpart_gpt_sgdisk(device, layout) + else: + exec_mkpart_gpt_sfdisk(device, layout) + read_parttbl(device) From b15f97de2ece75b10a309db7c0888fbc8f83a1f2 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 6 Aug 2025 13:15:00 +0200 Subject: [PATCH 055/150] test: drop citestcase from al* sources tests (#6318) --- tests/unittests/sources/test_aliyun.py | 357 ++++++++---------- tests/unittests/sources/test_altcloud.py | 452 ++++++++++------------- 2 files changed, 350 insertions(+), 459 deletions(-) diff --git a/tests/unittests/sources/test_aliyun.py b/tests/unittests/sources/test_aliyun.py index ca635019414..5a201dbdc19 100644 --- a/tests/unittests/sources/test_aliyun.py +++ b/tests/unittests/sources/test_aliyun.py @@ -7,13 +7,11 @@ import pytest import responses -from cloudinit import helpers from cloudinit.sources import DataSourceAliYun as ay from cloudinit.sources.helpers.aliyun import ( convert_ecs_metadata_network_config, ) from cloudinit.util import load_json -from tests.unittests import helpers as test_helpers DEFAULT_METADATA_RAW = r"""{ "disks": { @@ -97,186 +95,149 @@ - echo hello world > /tmp/vendor""" -class TestAliYunDatasource(test_helpers.CiTestCase): - def setUp(self): - super(TestAliYunDatasource, self).setUp() - cfg = {"datasource": {"AliYun": {"timeout": "1", "max_wait": "1"}}} - distro = {} - paths = helpers.Paths({"run_dir": self.tmp_dir()}) - self.ds = ay.DataSourceAliYun(cfg, distro, paths) - self.metadata_address = self.ds.metadata_urls[0] - - @property - def default_metadata(self): - return DEFAULT_METADATA - - @property - def default_userdata(self): - return DEFAULT_USERDATA - - @property - def default_vendordata(self): - return DEFAULT_VENDORDATA - - @property - def metadata_url(self): - return ( - os.path.join( - self.metadata_address, - self.ds.min_metadata_version, - "meta-data", - ) - + "/" - ) - - @property - def metadata_all_url(self): - return ( - os.path.join( - self.metadata_address, - self.ds.min_metadata_version, - "meta-data", - ) - + "/all" +@pytest.fixture +def ds(paths): + cfg = {"datasource": {"AliYun": {"timeout": "1", "max_wait": "1"}}} + distro = {} + return ay.DataSourceAliYun(cfg, distro, paths) + + +@pytest.fixture +def metadata_address(ds): + return ds.metadata_urls[0] + + +def register_mock_metaserver(base_url, data): + def register_helper(register, base_url, body): + if isinstance(body, str): + register(base_url, body) + elif isinstance(body, list): + register(base_url.rstrip("/"), "\n".join(body) + "\n") + elif isinstance(body, dict): + if not body: + register(base_url.rstrip("/") + "/", "not found", status=404) + vals = [] + for k, v in body.items(): + if isinstance(v, (str, list)): + suffix = k.rstrip("/") + else: + suffix = k.rstrip("/") + "/" + vals.append(suffix) + url = base_url.rstrip("/") + "/" + suffix + register_helper(register, url, v) + register(base_url, "\n".join(vals) + "\n") + + register = functools.partial(responses.add, responses.GET) + register_helper(register, base_url, data) + + +@pytest.fixture +def regist_default_server(ds, metadata_address): + metadata_url = ( + os.path.join( + metadata_address, + ds.min_metadata_version, + "meta-data", ) + + "/" + ) + register_mock_metaserver(metadata_url, DEFAULT_METADATA) - @property - def userdata_url(self): - return os.path.join( - self.metadata_address, self.ds.min_metadata_version, "user-data" - ) + userdata_url = os.path.join( + metadata_address, ds.min_metadata_version, "user-data" + ) + register_mock_metaserver(userdata_url, DEFAULT_USERDATA) - @property - def vendordata_url(self): - return os.path.join( - self.metadata_address, self.ds.min_metadata_version, "vendor-data" - ) + vendordata_url = os.path.join( + metadata_address, ds.min_metadata_version, "vendor-data" + ) + register_mock_metaserver(vendordata_url, DEFAULT_USERDATA) # EC2 provides an instance-identity document which must return 404 here # for this test to pass. - @property - def default_identity(self): - return {} - - @property - def identity_url(self): - return os.path.join( - self.metadata_address, - self.ds.min_metadata_version, - "dynamic", - "instance-identity", + default_identity = {} + identity_url = os.path.join( + metadata_address, + ds.min_metadata_version, + "dynamic", + "instance-identity", + ) + register_mock_metaserver(identity_url, default_identity) + + token_url = os.path.join(metadata_address, "latest", "api", "token") + responses.add(responses.PUT, token_url, "API-TOKEN") + + +@pytest.fixture +def regist_json_meta_path(ds, metadata_address): + metadata_all_url = ( + os.path.join( + metadata_address, + ds.min_metadata_version, + "meta-data", ) + + "/all" + ) + register_mock_metaserver(metadata_all_url, DEFAULT_METADATA_RAW) - @property - def token_url(self): - return os.path.join( - self.metadata_address, - "latest", - "api", - "token", - ) - def register_mock_metaserver(self, base_url, data): - def register_helper(register, base_url, body): - if isinstance(body, str): - register(base_url, body) - elif isinstance(body, list): - register(base_url.rstrip("/"), "\n".join(body) + "\n") - elif isinstance(body, dict): - if not body: - register( - base_url.rstrip("/") + "/", "not found", status=404 - ) - vals = [] - for k, v in body.items(): - if isinstance(v, (str, list)): - suffix = k.rstrip("/") - else: - suffix = k.rstrip("/") + "/" - vals.append(suffix) - url = base_url.rstrip("/") + "/" + suffix - register_helper(register, url, v) - register(base_url, "\n".join(vals) + "\n") - - register = functools.partial(responses.add, responses.GET) - register_helper(register, base_url, data) - - def regist_default_server(self, register_json_meta_path=True): - self.register_mock_metaserver(self.metadata_url, self.default_metadata) - if register_json_meta_path: - self.register_mock_metaserver( - self.metadata_all_url, DEFAULT_METADATA_RAW - ) - self.register_mock_metaserver(self.userdata_url, self.default_userdata) - self.register_mock_metaserver( - self.vendordata_url, self.default_userdata - ) - - self.register_mock_metaserver(self.identity_url, self.default_identity) - responses.add(responses.PUT, self.token_url, "API-TOKEN") +class TestAliYunDatasource: - def _test_get_data(self): - self.assertEqual(self.ds.metadata, self.default_metadata) - self.assertEqual( - self.ds.userdata_raw, self.default_userdata.encode("utf8") - ) + def _test_get_data(self, ds): + assert ds.metadata == DEFAULT_METADATA + assert ds.userdata_raw == DEFAULT_USERDATA.encode("utf8") - def _test_get_sshkey(self): + def _test_get_sshkey(self, ds): pub_keys = [ v["openssh-key"] - for (_, v) in self.default_metadata["public-keys"].items() + for (_, v) in DEFAULT_METADATA["public-keys"].items() ] - self.assertEqual(self.ds.get_public_ssh_keys(), pub_keys) + assert ds.get_public_ssh_keys() == pub_keys - def _test_get_iid(self): - self.assertEqual( - self.default_metadata["instance-id"], self.ds.get_instance_id() - ) + def _test_get_iid(self, ds): + assert DEFAULT_METADATA["instance-id"] == ds.get_instance_id() - def _test_host_name(self): - self.assertEqual( - self.default_metadata["hostname"], self.ds.get_hostname().hostname - ) + def _test_host_name(self, ds): + assert DEFAULT_METADATA["hostname"] == ds.get_hostname().hostname @responses.activate + @pytest.mark.usefixtures("regist_default_server", "regist_json_meta_path") @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") - def test_with_mock_server(self, m_is_aliyun, m_resolv): + def test_with_mock_server(self, m_is_aliyun, m_resolv, ds): m_is_aliyun.return_value = True - self.regist_default_server() - ret = self.ds.get_data() - self.assertEqual(True, ret) - self.assertEqual(1, m_is_aliyun.call_count) - self._test_get_data() - self._test_get_sshkey() - self._test_get_iid() - self._test_host_name() - self.assertEqual("aliyun", self.ds.cloud_name) - self.assertEqual("aliyun", self.ds.platform) - self.assertEqual( - "metadata (http://100.100.100.200)", self.ds.subplatform - ) + ret = ds.get_data() + assert True is ret + assert 1 == m_is_aliyun.call_count + self._test_get_data(ds) + self._test_get_sshkey(ds) + self._test_get_iid(ds) + self._test_host_name(ds) + assert "aliyun" == ds.cloud_name + assert "aliyun" == ds.platform + assert "metadata (http://100.100.100.200)" == ds.subplatform @responses.activate + @pytest.mark.usefixtures("regist_default_server") @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") - def test_with_mock_server_without_json_path(self, m_is_aliyun, m_resolv): + def test_with_mock_server_without_json_path( + self, m_is_aliyun, m_resolv, ds + ): m_is_aliyun.return_value = True - self.regist_default_server(register_json_meta_path=False) - ret = self.ds.get_data() - self.assertEqual(True, ret) - self.assertEqual(1, m_is_aliyun.call_count) - self._test_get_data() - self._test_get_sshkey() - self._test_get_iid() - self._test_host_name() - self.assertEqual("aliyun", self.ds.cloud_name) - self.assertEqual("aliyun", self.ds.platform) - self.assertEqual( - "metadata (http://100.100.100.200)", self.ds.subplatform - ) + ret = ds.get_data() + assert True is ret + assert 1 == m_is_aliyun.call_count + self._test_get_data(ds) + self._test_get_sshkey(ds) + self._test_get_iid(ds) + self._test_host_name(ds) + assert "aliyun" == ds.cloud_name + assert "aliyun" == ds.platform + assert "metadata (http://100.100.100.200)" == ds.subplatform @responses.activate + @pytest.mark.usefixtures("regist_default_server", "regist_json_meta_path") @mock.patch("cloudinit.net.ephemeral.EphemeralIPv6Network") @mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network") @mock.patch("cloudinit.sources.DataSourceEc2.util.is_resolvable") @@ -294,6 +255,8 @@ def test_aliyun_local_with_mock_server( m_resolva, m_net4, m_net6, + ds, + paths, ): m_is_aliyun.return_value = True m_fallback_nic.return_value = "eth9" @@ -307,67 +270,57 @@ def test_aliyun_local_with_mock_server( m_is_bsd.return_value = False cfg = {"datasource": {"AliYun": {"timeout": "1", "max_wait": "1"}}} distro = mock.MagicMock() - paths = helpers.Paths({"run_dir": self.tmp_dir()}) - self.ds = ay.DataSourceAliYunLocal(cfg, distro, paths) - self.regist_default_server() - ret = self.ds.get_data() - self.assertEqual(True, ret) - self.assertEqual(1, m_is_aliyun.call_count) - self._test_get_data() - self._test_get_sshkey() - self._test_get_iid() - self._test_host_name() - self.assertEqual("aliyun", self.ds.cloud_name) - self.assertEqual("aliyun", self.ds.platform) - self.assertEqual( - "metadata (http://100.100.100.200)", self.ds.subplatform - ) + ds = ay.DataSourceAliYunLocal(cfg, distro, paths) + ret = ds.get_data() + assert True is ret + assert 1 == m_is_aliyun.call_count + self._test_get_data(ds) + self._test_get_sshkey(ds) + self._test_get_iid(ds) + self._test_host_name(ds) + assert "aliyun" == ds.cloud_name + assert "aliyun" == ds.platform + assert "metadata (http://100.100.100.200)" == ds.subplatform @responses.activate + @pytest.mark.usefixtures("regist_default_server", "regist_json_meta_path") @mock.patch("cloudinit.sources.DataSourceAliYun._is_aliyun") - def test_returns_false_when_not_on_aliyun(self, m_is_aliyun): + def test_returns_false_when_not_on_aliyun(self, m_is_aliyun, ds): """If is_aliyun returns false, then get_data should return False.""" m_is_aliyun.return_value = False - self.regist_default_server() - ret = self.ds.get_data() - self.assertEqual(1, m_is_aliyun.call_count) - self.assertEqual(False, ret) + ret = ds.get_data() + assert 1 == m_is_aliyun.call_count + assert False is ret def test_parse_public_keys(self): public_keys = {} - self.assertEqual(ay.parse_public_keys(public_keys), []) + assert ay.parse_public_keys(public_keys) == [] public_keys = {"key-pair-0": "ssh-key-0"} - self.assertEqual( - ay.parse_public_keys(public_keys), [public_keys["key-pair-0"]] - ) + assert ay.parse_public_keys(public_keys) == [public_keys["key-pair-0"]] public_keys = {"key-pair-0": "ssh-key-0", "key-pair-1": "ssh-key-1"} - self.assertEqual( - set(ay.parse_public_keys(public_keys)), - set([public_keys["key-pair-0"], public_keys["key-pair-1"]]), + assert set(ay.parse_public_keys(public_keys)) == set( + [public_keys["key-pair-0"], public_keys["key-pair-1"]] ) public_keys = {"key-pair-0": ["ssh-key-0", "ssh-key-1"]} - self.assertEqual( - ay.parse_public_keys(public_keys), public_keys["key-pair-0"] - ) + assert ay.parse_public_keys(public_keys) == public_keys["key-pair-0"] public_keys = {"key-pair-0": {"openssh-key": []}} - self.assertEqual(ay.parse_public_keys(public_keys), []) + assert ay.parse_public_keys(public_keys) == [] public_keys = {"key-pair-0": {"openssh-key": "ssh-key-0"}} - self.assertEqual( - ay.parse_public_keys(public_keys), - [public_keys["key-pair-0"]["openssh-key"]], - ) + assert ay.parse_public_keys(public_keys) == [ + public_keys["key-pair-0"]["openssh-key"] + ] public_keys = { "key-pair-0": {"openssh-key": ["ssh-key-0", "ssh-key-1"]} } - self.assertEqual( - ay.parse_public_keys(public_keys), - public_keys["key-pair-0"]["openssh-key"], + assert ( + ay.parse_public_keys(public_keys) + == public_keys["key-pair-0"]["openssh-key"] ) def test_route_metric_calculated_with_multiple_network_cards(self): @@ -432,7 +385,7 @@ def test_route_metric_calculated_with_multiple_network_cards(self): assert "dhcp4-overrides" not in met0 -class TestIsAliYun(test_helpers.CiTestCase): +class TestIsAliYun: ALIYUN_PRODUCT = "Alibaba Cloud ECS" read_dmi_data_expected = [mock.call("system-product-name")] @@ -441,27 +394,21 @@ def test_true_on_aliyun_product(self, m_read_dmi_data): """Should return true if the dmi product data has expected value.""" m_read_dmi_data.return_value = self.ALIYUN_PRODUCT ret = ay._is_aliyun() - self.assertEqual( - self.read_dmi_data_expected, m_read_dmi_data.call_args_list - ) - self.assertEqual(True, ret) + assert self.read_dmi_data_expected == m_read_dmi_data.call_args_list + assert True is ret @mock.patch("cloudinit.sources.DataSourceAliYun.dmi.read_dmi_data") def test_false_on_empty_string(self, m_read_dmi_data): """Should return false on empty value returned.""" m_read_dmi_data.return_value = "" ret = ay._is_aliyun() - self.assertEqual( - self.read_dmi_data_expected, m_read_dmi_data.call_args_list - ) - self.assertEqual(False, ret) + assert self.read_dmi_data_expected == m_read_dmi_data.call_args_list + assert False is ret @mock.patch("cloudinit.sources.DataSourceAliYun.dmi.read_dmi_data") def test_false_on_unknown_string(self, m_read_dmi_data): """Should return false on an unrelated string.""" m_read_dmi_data.return_value = "cubs win" ret = ay._is_aliyun() - self.assertEqual( - self.read_dmi_data_expected, m_read_dmi_data.call_args_list - ) - self.assertEqual(False, ret) + assert self.read_dmi_data_expected == m_read_dmi_data.call_args_list + assert False is ret diff --git a/tests/unittests/sources/test_altcloud.py b/tests/unittests/sources/test_altcloud.py index dba0f6a4d71..74a8d336b3b 100644 --- a/tests/unittests/sources/test_altcloud.py +++ b/tests/unittests/sources/test_altcloud.py @@ -12,11 +12,12 @@ import os import shutil -import tempfile + +import pytest import cloudinit.sources.DataSourceAltCloud as dsac -from cloudinit import dmi, helpers, subp, util -from tests.unittests.helpers import CiTestCase, mock +from cloudinit import subp, util +from tests.unittests.helpers import mock OS_UNAME_ORIG = getattr(os, "uname") @@ -73,381 +74,324 @@ def _data(key): return _data -class TestGetCloudType(CiTestCase): +@pytest.fixture +def force_x86_64(): + # We have a different code path for arm to deal with LP1243287 + # We have to switch arch to x86_64 to avoid test failure + force_arch("x86_64") + yield + # Return back to original arch + force_arch() + + +@pytest.mark.usefixtures("fake_filesystem", "force_x86_64") +class TestGetCloudType: """Test to exercise method: DataSourceAltCloud.get_cloud_type()""" - with_logs = True - - def setUp(self): - """Set up.""" - super(TestGetCloudType, self).setUp() - self.tmp = self.tmp_dir() - self.paths = helpers.Paths({"cloud_dir": self.tmp}) - self.dmi_data = dmi.read_dmi_data - # We have a different code path for arm to deal with LP1243287 - # We have to switch arch to x86_64 to avoid test failure - force_arch("x86_64") - - def tearDown(self): - # Reset - dmi.read_dmi_data = self.dmi_data - force_arch() - super().tearDown() - - def test_cloud_info_file_ioerror(self): + def test_cloud_info_file_ioerror(self, caplog, paths, tmp_path): """Return UNKNOWN when /etc/sysconfig/cloud-info exists but errors.""" - self.assertEqual("/etc/sysconfig/cloud-info", dsac.CLOUD_INFO_FILE) - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + assert "/etc/sysconfig/cloud-info" == dsac.CLOUD_INFO_FILE + dsrc = dsac.DataSourceAltCloud({}, None, paths) # Attempting to read the directory generates IOError - with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.tmp): - self.assertEqual("UNKNOWN", dsrc.get_cloud_type()) - self.assertIn( - "[Errno 21] Is a directory: '%s'" % self.tmp, self.logs.getvalue() - ) + with mock.patch.object(dsac, "CLOUD_INFO_FILE", str(tmp_path)): + assert "UNKNOWN" == dsrc.get_cloud_type() + assert "[Errno 21] Is a directory: '%s'" % tmp_path in caplog.text - def test_cloud_info_file(self): + def test_cloud_info_file(self, paths): """Return uppercase stripped content from /etc/sysconfig/cloud-info.""" - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - cloud_info = self.tmp_path("cloud-info", dir=self.tmp) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + cloud_info = "cloud-info" util.write_file(cloud_info, " OverRiDdeN CloudType ") # Attempting to read the directory generates IOError with mock.patch.object(dsac, "CLOUD_INFO_FILE", cloud_info): - self.assertEqual("OVERRIDDEN CLOUDTYPE", dsrc.get_cloud_type()) + assert "OVERRIDDEN CLOUDTYPE" == dsrc.get_cloud_type() - def test_rhev(self): + @mock.patch("cloudinit.dmi.read_dmi_data", side_effect=_dmi_data("RHEV")) + def test_rhev(self, m_read_dmi_data, paths): """ Test method get_cloud_type() for RHEVm systems. Forcing read_dmi_data return to match a RHEVm system: RHEV Hypervisor """ - dmi.read_dmi_data = _dmi_data("RHEV") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual("RHEV", dsrc.get_cloud_type()) - - def test_vsphere(self): + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert "RHEV" == dsrc.get_cloud_type() + + @mock.patch( + "cloudinit.dmi.read_dmi_data", + side_effect=_dmi_data("VMware Virtual Platform"), + ) + def test_vsphere(self, m_read_dmi_data, paths): """ Test method get_cloud_type() for vSphere systems. Forcing read_dmi_data return to match a vSphere system: RHEV Hypervisor """ - dmi.read_dmi_data = _dmi_data("VMware Virtual Platform") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual("VSPHERE", dsrc.get_cloud_type()) - - def test_unknown(self): + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert "VSPHERE" == dsrc.get_cloud_type() + + @mock.patch( + "cloudinit.dmi.read_dmi_data", + side_effect=_dmi_data("Unrecognized Platform"), + ) + def test_unknown(self, m_read_dmi_data, paths): """ Test method get_cloud_type() for unknown systems. Forcing read_dmi_data return to match an unrecognized return. """ - dmi.read_dmi_data = _dmi_data("Unrecognized Platform") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual("UNKNOWN", dsrc.get_cloud_type()) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert "UNKNOWN" == dsrc.get_cloud_type() -class TestGetDataCloudInfoFile(CiTestCase): +@pytest.mark.usefixtures("fake_filesystem") +class TestGetDataCloudInfoFile: """ Test to exercise method: DataSourceAltCloud.get_data() With a contrived CLOUD_INFO_FILE """ - def setUp(self): - """Set up.""" - self.tmp = self.tmp_dir() - self.paths = helpers.Paths( - {"cloud_dir": self.tmp, "run_dir": self.tmp} - ) - self.cloud_info_file = self.tmp_path("cloud-info", dir=self.tmp) + CLOUD_INFO_FILE = "cloud-info" - def test_rhev(self): + def test_rhev(self, paths): """Success Test module get_data() forcing RHEV.""" - util.write_file(self.cloud_info_file, "RHEV") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + util.write_file(self.CLOUD_INFO_FILE, "RHEV") + dsrc = dsac.DataSourceAltCloud({}, None, paths) dsrc.user_data_rhevm = lambda: True - with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.cloud_info_file): - self.assertEqual(True, dsrc.get_data()) - self.assertEqual("altcloud", dsrc.cloud_name) - self.assertEqual("altcloud", dsrc.platform_type) - self.assertEqual("rhev (/dev/fd0)", dsrc.subplatform) + with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.CLOUD_INFO_FILE): + assert True is dsrc.get_data() + assert "altcloud" == dsrc.cloud_name + assert "altcloud" == dsrc.platform_type + assert "rhev (/dev/fd0)" == dsrc.subplatform - def test_vsphere(self): + def test_vsphere(self, paths): """Success Test module get_data() forcing VSPHERE.""" - util.write_file(self.cloud_info_file, "VSPHERE") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + util.write_file(self.CLOUD_INFO_FILE, "VSPHERE") + dsrc = dsac.DataSourceAltCloud({}, None, paths) dsrc.user_data_vsphere = lambda: True - with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.cloud_info_file): - self.assertEqual(True, dsrc.get_data()) - self.assertEqual("altcloud", dsrc.cloud_name) - self.assertEqual("altcloud", dsrc.platform_type) - self.assertEqual("vsphere (unknown)", dsrc.subplatform) + with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.CLOUD_INFO_FILE): + assert True is dsrc.get_data() + assert "altcloud" == dsrc.cloud_name + assert "altcloud" == dsrc.platform_type + assert "vsphere (unknown)" == dsrc.subplatform - def test_fail_rhev(self): + def test_fail_rhev(self, paths): """Failure Test module get_data() forcing RHEV.""" - util.write_file(self.cloud_info_file, "RHEV") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + util.write_file(self.CLOUD_INFO_FILE, "RHEV") + dsrc = dsac.DataSourceAltCloud({}, None, paths) dsrc.user_data_rhevm = lambda: False - with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.cloud_info_file): - self.assertEqual(False, dsrc.get_data()) + with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.CLOUD_INFO_FILE): + assert False is dsrc.get_data() - def test_fail_vsphere(self): + def test_fail_vsphere(self, paths): """Failure Test module get_data() forcing VSPHERE.""" - util.write_file(self.cloud_info_file, "VSPHERE") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + util.write_file(self.CLOUD_INFO_FILE, "VSPHERE") + dsrc = dsac.DataSourceAltCloud({}, None, paths) dsrc.user_data_vsphere = lambda: False - with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.cloud_info_file): - self.assertEqual(False, dsrc.get_data()) + with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.CLOUD_INFO_FILE): + assert False is dsrc.get_data() - def test_unrecognized(self): + def test_unrecognized(self, paths): """Failure Test module get_data() forcing unrecognized.""" - util.write_file(self.cloud_info_file, "unrecognized") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.cloud_info_file): - self.assertEqual(False, dsrc.get_data()) + util.write_file(self.CLOUD_INFO_FILE, "unrecognized") + dsrc = dsac.DataSourceAltCloud({}, None, paths) + with mock.patch.object(dsac, "CLOUD_INFO_FILE", self.CLOUD_INFO_FILE): + assert False is dsrc.get_data() -class TestGetDataNoCloudInfoFile(CiTestCase): +@pytest.fixture +def fake_dsca_cloud_info(): + dsac.CLOUD_INFO_FILE = "no such file" + yield + dsac.CLOUD_INFO_FILE = "/etc/sysconfig/cloud-info" + + +@pytest.mark.usefixtures( + "fake_filesystem", "force_x86_64", "fake_dsca_cloud_info" +) +class TestGetDataNoCloudInfoFile: """ Test to exercise method: DataSourceAltCloud.get_data() Without a CLOUD_INFO_FILE """ - def setUp(self): - """Set up.""" - self.tmp = self.tmp_dir() - self.paths = helpers.Paths( - {"cloud_dir": self.tmp, "run_dir": self.tmp} - ) - self.dmi_data = dmi.read_dmi_data - dsac.CLOUD_INFO_FILE = "no such file" - # We have a different code path for arm to deal with LP1243287 - # We have to switch arch to x86_64 to avoid test failure - force_arch("x86_64") - - def tearDown(self): - # Reset - dsac.CLOUD_INFO_FILE = "/etc/sysconfig/cloud-info" - dmi.read_dmi_data = self.dmi_data - # Return back to original arch - force_arch() - super().tearDown() - - def test_rhev_no_cloud_file(self): + @mock.patch( + "cloudinit.dmi.read_dmi_data", side_effect=_dmi_data("RHEV Hypervisor") + ) + def test_rhev_no_cloud_file(self, m_read_dmi_data, paths): """Test No cloud info file module get_data() forcing RHEV.""" - - dmi.read_dmi_data = _dmi_data("RHEV Hypervisor") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + dsrc = dsac.DataSourceAltCloud({}, None, paths) dsrc.user_data_rhevm = lambda: True - self.assertEqual(True, dsrc.get_data()) + assert True is dsrc.get_data() - def test_vsphere_no_cloud_file(self): + @mock.patch( + "cloudinit.dmi.read_dmi_data", + side_effect=_dmi_data("VMware Virtual Platform"), + ) + def test_vsphere_no_cloud_file(self, m_read_dmi_data, paths): """Test No cloud info file module get_data() forcing VSPHERE.""" - - dmi.read_dmi_data = _dmi_data("VMware Virtual Platform") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) + dsrc = dsac.DataSourceAltCloud({}, None, paths) dsrc.user_data_vsphere = lambda: True - self.assertEqual(True, dsrc.get_data()) + assert True is dsrc.get_data() - def test_failure_no_cloud_file(self): + @mock.patch( + "cloudinit.dmi.read_dmi_data", + side_effect=_dmi_data("Unrecognized Platform"), + ) + def test_failure_no_cloud_file(self, m_read_dmi_data, paths): """Test No cloud info file module get_data() forcing unrecognized.""" - - dmi.read_dmi_data = _dmi_data("Unrecognized Platform") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.get_data()) - - -class TestUserDataRhevm(CiTestCase): + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.get_data() + + +@pytest.fixture +def user_data(tmp_path): + mount_dir = str(tmp_path) + _write_user_data_files(mount_dir, "test user data") + yield + _remove_user_data_files(mount_dir) + shutil.rmtree(mount_dir) + + +@pytest.mark.usefixtures("user_data") +@mock.patch( + "cloudinit.sources.DataSourceAltCloud.modprobe_floppy", + return_value=None, +) +@mock.patch( + "cloudinit.sources.DataSourceAltCloud.util.udevadm_settle", + return_value=("", ""), +) +@mock.patch("cloudinit.sources.DataSourceAltCloud.util.mount_cb") +class TestUserDataRhevm: """ Test to exercise method: DataSourceAltCloud.user_data_rhevm() """ - def setUp(self): - """Set up.""" - self.paths = helpers.Paths({"cloud_dir": "/tmp"}) - self.mount_dir = self.tmp_dir() - _write_user_data_files(self.mount_dir, "test user data") - self.add_patch( - "cloudinit.sources.DataSourceAltCloud.modprobe_floppy", - "m_modprobe_floppy", - return_value=None, - ) - self.add_patch( - "cloudinit.sources.DataSourceAltCloud.util.udevadm_settle", - "m_udevadm_settle", - return_value=("", ""), - ) - self.add_patch( - "cloudinit.sources.DataSourceAltCloud.util.mount_cb", "m_mount_cb" - ) - - def test_mount_cb_fails(self): + def test_mount_cb_fails( + self, m_mount_cb, m_udevadm_settle, m_modprobe_floppy, paths + ): """Test user_data_rhevm() where mount_cb fails.""" + m_mount_cb.side_effect = util.MountFailedError("Failed Mount") + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_rhevm() - self.m_mount_cb.side_effect = util.MountFailedError("Failed Mount") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_rhevm()) - - def test_modprobe_fails(self): + def test_modprobe_fails( + self, m_mount_cb, m_udevadm_settle, m_modprobe_floppy, paths + ): """Test user_data_rhevm() where modprobe fails.""" - - self.m_modprobe_floppy.side_effect = subp.ProcessExecutionError( + m_modprobe_floppy.side_effect = subp.ProcessExecutionError( "Failed modprobe" ) - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_rhevm()) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_rhevm() - def test_no_modprobe_cmd(self): + def test_no_modprobe_cmd( + self, m_mount_cb, m_udevadm_settle, m_modprobe_floppy, paths + ): """Test user_data_rhevm() with no modprobe command.""" - - self.m_modprobe_floppy.side_effect = subp.ProcessExecutionError( + m_modprobe_floppy.side_effect = subp.ProcessExecutionError( "No such file or dir" ) - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_rhevm()) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_rhevm() - def test_udevadm_fails(self): + def test_udevadm_fails( + self, m_mount_cb, m_udevadm_settle, m_modprobe_floppy, paths + ): """Test user_data_rhevm() where udevadm fails.""" - - self.m_udevadm_settle.side_effect = subp.ProcessExecutionError( + m_udevadm_settle.side_effect = subp.ProcessExecutionError( "Failed settle." ) - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_rhevm()) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_rhevm() - def test_no_udevadm_cmd(self): + def test_no_udevadm_cmd( + self, m_mount_cb, m_udevadm_settle, m_modprobe_floppy, paths + ): """Test user_data_rhevm() with no udevadm command.""" + m_udevadm_settle.side_effect = OSError("No such file or dir") + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_rhevm() - self.m_udevadm_settle.side_effect = OSError("No such file or dir") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_rhevm()) - -class TestUserDataVsphere(CiTestCase): +@pytest.mark.usefixtures("user_data") +class TestUserDataVsphere: """ Test to exercise method: DataSourceAltCloud.user_data_vsphere() """ - def setUp(self): - """Set up.""" - self.tmp = self.tmp_dir() - self.paths = helpers.Paths({"cloud_dir": self.tmp}) - self.mount_dir = tempfile.mkdtemp() - - _write_user_data_files(self.mount_dir, "test user data") - - def tearDown(self): - # Reset - - _remove_user_data_files(self.mount_dir) - - # Attempt to remove the temp dir ignoring errors - try: - shutil.rmtree(self.mount_dir) - except OSError: - pass - - dsac.CLOUD_INFO_FILE = "/etc/sysconfig/cloud-info" - super().tearDown() - @mock.patch("cloudinit.sources.DataSourceAltCloud.util.find_devs_with") @mock.patch("cloudinit.sources.DataSourceAltCloud.util.mount_cb") - def test_user_data_vsphere_no_cdrom(self, m_mount_cb, m_find_devs_with): + def test_user_data_vsphere_no_cdrom( + self, m_mount_cb, m_find_devs_with, paths + ): """Test user_data_vsphere() where mount_cb fails.""" m_mount_cb.return_value = [] - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_vsphere()) - self.assertEqual(0, m_mount_cb.call_count) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_vsphere() + assert 0 == m_mount_cb.call_count @mock.patch("cloudinit.sources.DataSourceAltCloud.util.find_devs_with") @mock.patch("cloudinit.sources.DataSourceAltCloud.util.mount_cb") - def test_user_data_vsphere_mcb_fail(self, m_mount_cb, m_find_devs_with): + def test_user_data_vsphere_mcb_fail( + self, m_mount_cb, m_find_devs_with, paths + ): """Test user_data_vsphere() where mount_cb fails.""" m_find_devs_with.return_value = ["/dev/mock/cdrom"] m_mount_cb.side_effect = util.MountFailedError("Unable To mount") - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - self.assertEqual(False, dsrc.user_data_vsphere()) - self.assertEqual(1, m_find_devs_with.call_count) - self.assertEqual(1, m_mount_cb.call_count) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + assert False is dsrc.user_data_vsphere() + assert 1 == m_find_devs_with.call_count + assert 1 == m_mount_cb.call_count @mock.patch("cloudinit.sources.DataSourceAltCloud.util.find_devs_with") @mock.patch("cloudinit.sources.DataSourceAltCloud.util.mount_cb") - def test_user_data_vsphere_success(self, m_mount_cb, m_find_devs_with): + def test_user_data_vsphere_success( + self, m_mount_cb, m_find_devs_with, tmp_path, paths + ): """Test user_data_vsphere() where successful.""" m_find_devs_with.return_value = ["/dev/mock/cdrom"] m_mount_cb.return_value = "raw userdata from cdrom" - dsrc = dsac.DataSourceAltCloud({}, None, self.paths) - cloud_info = self.tmp_path("cloud-info", dir=self.tmp) + dsrc = dsac.DataSourceAltCloud({}, None, paths) + cloud_info = tmp_path / "cloud-info" util.write_file(cloud_info, "VSPHERE") - self.assertEqual(True, dsrc.user_data_vsphere()) + assert True is dsrc.user_data_vsphere() m_find_devs_with.assert_called_once_with("LABEL=CDROM") m_mount_cb.assert_called_once_with( "/dev/mock/cdrom", dsac.read_user_data_callback ) with mock.patch.object(dsrc, "get_cloud_type", return_value="VSPHERE"): - self.assertEqual("vsphere (/dev/mock/cdrom)", dsrc.subplatform) + assert "vsphere (/dev/mock/cdrom)" == dsrc.subplatform -class TestReadUserDataCallback(CiTestCase): +@pytest.mark.usefixtures("user_data") +class TestReadUserDataCallback: """ Test to exercise method: DataSourceAltCloud.read_user_data_callback() """ - def setUp(self): - """Set up.""" - self.paths = helpers.Paths({"cloud_dir": "/tmp"}) - self.mount_dir = tempfile.mkdtemp() - - _write_user_data_files(self.mount_dir, "test user data") - - def tearDown(self): - # Reset - - _remove_user_data_files(self.mount_dir) - - # Attempt to remove the temp dir ignoring errors - try: - shutil.rmtree(self.mount_dir) - except OSError: - pass - super().tearDown() - - def test_callback_both(self): + def test_callback_both(self, tmp_path): """Test read_user_data_callback() with both files.""" + assert "test user data" == dsac.read_user_data_callback(str(tmp_path)) - self.assertEqual( - "test user data", dsac.read_user_data_callback(self.mount_dir) - ) - - def test_callback_dc(self): + def test_callback_dc(self, tmp_path): """Test read_user_data_callback() with only DC file.""" + _remove_user_data_files(str(tmp_path), dc_file=False, non_dc_file=True) + assert "test user data" == dsac.read_user_data_callback(str(tmp_path)) - _remove_user_data_files( - self.mount_dir, dc_file=False, non_dc_file=True - ) - - self.assertEqual( - "test user data", dsac.read_user_data_callback(self.mount_dir) - ) - - def test_callback_non_dc(self): + def test_callback_non_dc(self, tmp_path): """Test read_user_data_callback() with only non-DC file.""" + _remove_user_data_files(str(tmp_path), dc_file=True, non_dc_file=False) + assert "test user data" == dsac.read_user_data_callback(str(tmp_path)) - _remove_user_data_files( - self.mount_dir, dc_file=True, non_dc_file=False - ) - - self.assertEqual( - "test user data", dsac.read_user_data_callback(self.mount_dir) - ) - - def test_callback_none(self): + def test_callback_none(self, tmp_path): """Test read_user_data_callback() no files are found.""" - - _remove_user_data_files(self.mount_dir) - self.assertIsNone(dsac.read_user_data_callback(self.mount_dir)) + _remove_user_data_files(str(tmp_path)) + assert dsac.read_user_data_callback(str(tmp_path)) is None def force_arch(arch=None): From 23e728cc1930090b98b27bac9fb87a227f61234d Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 6 Aug 2025 16:24:38 +0200 Subject: [PATCH 056/150] test: drop citestcase from c and d sources (#6336) --- tests/unittests/sources/test_cloudsigma.py | 136 +++++----- tests/unittests/sources/test_configdrive.py | 250 +++++++++---------- tests/unittests/sources/test_digitalocean.py | 168 +++++++------ 3 files changed, 262 insertions(+), 292 deletions(-) diff --git a/tests/unittests/sources/test_cloudsigma.py b/tests/unittests/sources/test_cloudsigma.py index 0410c3c7008..c63bfbd0fbc 100644 --- a/tests/unittests/sources/test_cloudsigma.py +++ b/tests/unittests/sources/test_cloudsigma.py @@ -4,10 +4,11 @@ import copy from unittest import mock -from cloudinit import distros, helpers, importer, sources +import pytest + +from cloudinit import distros, importer, sources from cloudinit.sources import DataSourceCloudSigma from cloudinit.sources.helpers.cloudsigma import Cepko -from tests.unittests import helpers as test_helpers SERVER_CONTEXT = { "cpu": 1000, @@ -40,104 +41,89 @@ def all(self): return self -class DataSourceCloudSigmaTest(test_helpers.CiTestCase): - def setUp(self): - super(DataSourceCloudSigmaTest, self).setUp() - self.paths = helpers.Paths({"run_dir": self.tmp_dir()}) - self.add_patch( - DS_PATH + ".override_ds_detect", - "m_is_container", - return_value=True, - ) +@pytest.fixture +def ds(mocker, paths): + mocker.patch(DS_PATH + ".override_ds_detect", return_value=True) + distro_cls = distros.fetch("ubuntu") + distro = distro_cls("ubuntu", cfg={}, paths=paths) + datasource = DataSourceCloudSigma.DataSourceCloudSigma( + sys_cfg={}, distro=distro, paths=paths + ) + datasource.cepko = CepkoMock(SERVER_CONTEXT) + return datasource - distro_cls = distros.fetch("ubuntu") - distro = distro_cls("ubuntu", cfg={}, paths=self.paths) - self.datasource = DataSourceCloudSigma.DataSourceCloudSigma( - sys_cfg={}, distro=distro, paths=self.paths - ) - self.datasource.cepko = CepkoMock(SERVER_CONTEXT) - def test_get_hostname(self): - self.datasource.get_data() - self.assertEqual( - "test_server", self.datasource.get_hostname().hostname - ) - self.datasource.metadata["name"] = "" - self.assertEqual("65b2fb23", self.datasource.get_hostname().hostname) +class TestDataSourceCloudSigma: + + def test_get_hostname(self, ds): + ds.get_data() + assert "test_server" == ds.get_hostname().hostname + ds.metadata["name"] = "" + assert "65b2fb23" == ds.get_hostname().hostname utf8_hostname = b"\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".decode("utf-8") - self.datasource.metadata["name"] = utf8_hostname - self.assertEqual("65b2fb23", self.datasource.get_hostname().hostname) - - def test_get_public_ssh_keys(self): - self.datasource.get_data() - self.assertEqual( - [SERVER_CONTEXT["meta"]["ssh_public_key"]], - self.datasource.get_public_ssh_keys(), - ) + ds.metadata["name"] = utf8_hostname + assert "65b2fb23" == ds.get_hostname().hostname - def test_get_instance_id(self): - self.datasource.get_data() - self.assertEqual( - SERVER_CONTEXT["uuid"], self.datasource.get_instance_id() - ) + def test_get_public_ssh_keys(self, ds): + ds.get_data() + assert [ + SERVER_CONTEXT["meta"]["ssh_public_key"] + ] == ds.get_public_ssh_keys() + + def test_get_instance_id(self, ds): + ds.get_data() + assert SERVER_CONTEXT["uuid"] == ds.get_instance_id() - def test_platform(self): + def test_platform(self, ds): """All platform-related attributes are set.""" - self.datasource.get_data() - self.assertEqual(self.datasource.cloud_name, "cloudsigma") - self.assertEqual(self.datasource.platform_type, "cloudsigma") - self.assertEqual(self.datasource.subplatform, "cepko (/dev/ttyS1)") - - def test_metadata(self): - self.datasource.get_data() - self.assertEqual(self.datasource.metadata, SERVER_CONTEXT) - - def test_user_data(self): - self.datasource.get_data() - self.assertEqual( - self.datasource.userdata_raw, - SERVER_CONTEXT["meta"]["cloudinit-user-data"], - ) + ds.get_data() + assert ds.cloud_name == "cloudsigma" + assert ds.platform_type == "cloudsigma" + assert ds.subplatform == "cepko (/dev/ttyS1)" + + def test_metadata(self, ds): + ds.get_data() + assert ds.metadata == SERVER_CONTEXT - def test_encoded_user_data(self): + def test_user_data(self, ds): + ds.get_data() + assert ds.userdata_raw == SERVER_CONTEXT["meta"]["cloudinit-user-data"] + def test_encoded_user_data(self, ds): encoded_context = copy.deepcopy(SERVER_CONTEXT) encoded_context["meta"]["base64_fields"] = "cloudinit-user-data" encoded_context["meta"]["cloudinit-user-data"] = "aGkgd29ybGQK" - self.datasource.cepko = CepkoMock(encoded_context) - self.datasource.get_data() + ds.cepko = CepkoMock(encoded_context) + ds.get_data() - self.assertEqual(self.datasource.userdata_raw, b"hi world\n") + assert ds.userdata_raw == b"hi world\n" - def test_vendor_data(self): - self.datasource.get_data() - self.assertEqual( - self.datasource.vendordata_raw, - SERVER_CONTEXT["vendor_data"]["cloudinit"], - ) + def test_vendor_data(self, ds): + ds.get_data() + assert ds.vendordata_raw == SERVER_CONTEXT["vendor_data"]["cloudinit"] - def test_lack_of_vendor_data(self): + def test_lack_of_vendor_data(self, ds): stripped_context = copy.deepcopy(SERVER_CONTEXT) del stripped_context["vendor_data"] - self.datasource.cepko = CepkoMock(stripped_context) - self.datasource.get_data() + ds.cepko = CepkoMock(stripped_context) + ds.get_data() - self.assertIsNone(self.datasource.vendordata_raw) + assert ds.vendordata_raw is None - def test_lack_of_cloudinit_key_in_vendor_data(self): + def test_lack_of_cloudinit_key_in_vendor_data(self, ds): stripped_context = copy.deepcopy(SERVER_CONTEXT) del stripped_context["vendor_data"]["cloudinit"] - self.datasource.cepko = CepkoMock(stripped_context) - self.datasource.get_data() + ds.cepko = CepkoMock(stripped_context) + ds.get_data() - self.assertIsNone(self.datasource.vendordata_raw) + assert ds.vendordata_raw is None -class DsLoads(test_helpers.TestCase): +class TestDsLoads: def test_get_datasource_list_returns_in_local(self): deps = (sources.DEP_FILESYSTEM,) ds_list = DataSourceCloudSigma.get_datasource_list(deps) - self.assertEqual(ds_list, [DataSourceCloudSigma.DataSourceCloudSigma]) + assert ds_list == [DataSourceCloudSigma.DataSourceCloudSigma] @mock.patch.object( importer, @@ -150,4 +136,4 @@ def test_list_sources_finds_ds(self): (sources.DEP_FILESYSTEM,), ["cloudinit.sources"], ) - self.assertEqual([DataSourceCloudSigma.DataSourceCloudSigma], found) + assert [DataSourceCloudSigma.DataSourceCloudSigma] == found diff --git a/tests/unittests/sources/test_configdrive.py b/tests/unittests/sources/test_configdrive.py index a649df8950a..089bdef4aa4 100644 --- a/tests/unittests/sources/test_configdrive.py +++ b/tests/unittests/sources/test_configdrive.py @@ -5,11 +5,13 @@ from contextlib import ExitStack from copy import copy, deepcopy +import pytest + from cloudinit import helpers, settings, util from cloudinit.net import eni, network_state from cloudinit.sources import DataSourceConfigDrive as ds from cloudinit.sources.helpers import openstack -from tests.unittests.helpers import CiTestCase, mock, populate_dir +from tests.unittests.helpers import mock, populate_dir PUBKEY = "ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n" EC2_META = { @@ -383,27 +385,23 @@ M_PATH = "cloudinit.sources.DataSourceConfigDrive." -class TestConfigDriveDataSource(CiTestCase): - def setUp(self): - super(TestConfigDriveDataSource, self).setUp() - self.add_patch( - M_PATH + "util.find_devs_with", "m_find_devs_with", return_value=[] - ) - self.tmp = self.tmp_dir() +class TestConfigDriveDataSource: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + mocker.patch(M_PATH + "util.find_devs_with", return_value=[]) + populate_dir(str(tmp_path), CFG_DRIVE_FILES_V2) - def test_ec2_metadata(self): - populate_dir(self.tmp, CFG_DRIVE_FILES_V2) - found = ds.read_config_drive(self.tmp) - self.assertTrue("ec2-metadata" in found) + def test_ec2_metadata(self, tmp_path): + found = ds.read_config_drive(tmp_path) + assert "ec2-metadata" in found ec2_md = found["ec2-metadata"] - self.assertEqual(EC2_META, ec2_md) + assert EC2_META == ec2_md - def test_dev_os_remap(self): - populate_dir(self.tmp, CFG_DRIVE_FILES_V2) + def test_dev_os_remap(self, tmp_path): cfg_ds = ds.DataSourceConfigDrive( settings.CFG_BUILTIN, None, helpers.Paths({}) ) - found = ds.read_config_drive(self.tmp) + found = ds.read_config_drive(tmp_path) cfg_ds.metadata = found["metadata"] name_tests = { "ami": "/dev/vda1", @@ -434,17 +432,16 @@ def exists_side_effect(): os.path, "exists", side_effect=exists_side_effect() ) ) - self.assertEqual(dev_name, cfg_ds.device_name_to_device(name)) + assert dev_name == cfg_ds.device_name_to_device(name) find_mock.assert_called_once_with(mock.ANY) - self.assertEqual(exists_mock.call_count, 2) + assert exists_mock.call_count == 2 - def test_dev_os_map(self): - populate_dir(self.tmp, CFG_DRIVE_FILES_V2) + def test_dev_os_map(self, tmp_path): cfg_ds = ds.DataSourceConfigDrive( settings.CFG_BUILTIN, None, helpers.Paths({}) ) - found = ds.read_config_drive(self.tmp) + found = ds.read_config_drive(tmp_path) os_md = found["metadata"] cfg_ds.metadata = os_md name_tests = { @@ -463,17 +460,16 @@ def test_dev_os_map(self): exists_mock = mocks.enter_context( mock.patch.object(os.path, "exists", return_value=True) ) - self.assertEqual(dev_name, cfg_ds.device_name_to_device(name)) + assert dev_name == cfg_ds.device_name_to_device(name) find_mock.assert_called_once_with(mock.ANY) exists_mock.assert_called_once_with(mock.ANY) - def test_dev_ec2_remap(self): - populate_dir(self.tmp, CFG_DRIVE_FILES_V2) + def test_dev_ec2_remap(self, tmp_path): cfg_ds = ds.DataSourceConfigDrive( settings.CFG_BUILTIN, None, helpers.Paths({}) ) - found = ds.read_config_drive(self.tmp) + found = ds.read_config_drive(tmp_path) ec2_md = found["ec2-metadata"] os_md = found["metadata"] cfg_ds.ec2_metadata = ec2_md @@ -499,18 +495,17 @@ def exists_side_effect(): with mock.patch.object( os.path, "exists", side_effect=exists_side_effect() ): - self.assertEqual(dev_name, cfg_ds.device_name_to_device(name)) + assert dev_name == cfg_ds.device_name_to_device(name) # We don't assert the call count for os.path.exists() because # not all of the entries in name_tests results in two calls to # that function. Specifically, 'root2k' doesn't seem to call # it at all. - def test_dev_ec2_map(self): - populate_dir(self.tmp, CFG_DRIVE_FILES_V2) + def test_dev_ec2_map(self, tmp_path): cfg_ds = ds.DataSourceConfigDrive( settings.CFG_BUILTIN, None, helpers.Paths({}) ) - found = ds.read_config_drive(self.tmp) + found = ds.read_config_drive(tmp_path) ec2_md = found["ec2-metadata"] os_md = found["metadata"] cfg_ds.ec2_metadata = ec2_md @@ -526,45 +521,43 @@ def test_dev_ec2_map(self): } for name, dev_name in name_tests.items(): with mock.patch.object(os.path, "exists", return_value=True): - self.assertEqual(dev_name, cfg_ds.device_name_to_device(name)) + assert dev_name == cfg_ds.device_name_to_device(name) - def test_dir_valid(self): + def test_dir_valid(self, tmp_path): """Verify a dir is read as such.""" - populate_dir(self.tmp, CFG_DRIVE_FILES_V2) - - found = ds.read_config_drive(self.tmp) + found = ds.read_config_drive(tmp_path) expected_md = copy(OSTACK_META) expected_md["instance-id"] = expected_md["uuid"] expected_md["local-hostname"] = expected_md["hostname"] - self.assertEqual(USER_DATA, found["userdata"]) - self.assertEqual(expected_md, found["metadata"]) - self.assertEqual(NETWORK_DATA, found["networkdata"]) - self.assertEqual(VENDOR_DATA, found["vendordata"]) - self.assertEqual(VENDOR_DATA2, found["vendordata2"]) - self.assertEqual(found["files"]["/etc/foo.cfg"], CONTENT_0) - self.assertEqual(found["files"]["/etc/bar/bar.cfg"], CONTENT_1) + assert USER_DATA == found["userdata"] + assert expected_md == found["metadata"] + assert NETWORK_DATA == found["networkdata"] + assert VENDOR_DATA == found["vendordata"] + assert VENDOR_DATA2 == found["vendordata2"] + assert found["files"]["/etc/foo.cfg"] == CONTENT_0 + assert found["files"]["/etc/bar/bar.cfg"] == CONTENT_1 - def test_seed_dir_valid_extra(self): + def test_seed_dir_valid_extra(self, tmp_path): """Verify extra files do not affect datasource validity.""" data = copy(CFG_DRIVE_FILES_V2) data["myfoofile.txt"] = "myfoocontent" data["openstack/latest/random-file.txt"] = "random-content" - populate_dir(self.tmp, data) + populate_dir(str(tmp_path), data) - found = ds.read_config_drive(self.tmp) + found = ds.read_config_drive(tmp_path) expected_md = copy(OSTACK_META) expected_md["instance-id"] = expected_md["uuid"] expected_md["local-hostname"] = expected_md["hostname"] - self.assertEqual(expected_md, found["metadata"]) + assert expected_md == found["metadata"] - def test_seed_dir_bad_json_metadata(self): + def test_seed_dir_bad_json_metadata(self, tmp_path): """Verify that bad json in metadata raises BrokenConfigDriveDir.""" data = copy(CFG_DRIVE_FILES_V2) @@ -572,27 +565,28 @@ def test_seed_dir_bad_json_metadata(self): data["openstack/2015-10-15/meta_data.json"] = "non-json garbage {}" data["openstack/latest/meta_data.json"] = "non-json garbage {}" - populate_dir(self.tmp, data) + populate_dir(str(tmp_path), data) - self.assertRaises( - openstack.BrokenMetadata, ds.read_config_drive, self.tmp - ) + with pytest.raises(openstack.BrokenMetadata): + ds.read_config_drive(tmp_path) - def test_seed_dir_no_configdrive(self): + def test_seed_dir_no_configdrive(self, tmp_path): """Verify that no metadata raises NonConfigDriveDir.""" - my_d = os.path.join(self.tmp, "non-configdrive") + my_d = os.path.join(tmp_path, "non-configdrive") data = copy(CFG_DRIVE_FILES_V2) data["myfoofile.txt"] = "myfoocontent" data["openstack/latest/random-file.txt"] = "random-content" data["content/foo"] = "foocontent" - self.assertRaises(openstack.NonReadable, ds.read_config_drive, my_d) + with pytest.raises(openstack.NonReadable): + ds.read_config_drive(my_d) - def test_seed_dir_missing(self): + def test_seed_dir_missing(self, tmp_path): """Verify that missing seed_dir raises NonConfigDriveDir.""" - my_d = os.path.join(self.tmp, "nonexistantdirectory") - self.assertRaises(openstack.NonReadable, ds.read_config_drive, my_d) + my_d = os.path.join(tmp_path, "nonexistantdirectory") + with pytest.raises(openstack.NonReadable): + ds.read_config_drive(my_d) def test_find_candidates(self): devs_with_answers = {} @@ -615,14 +609,12 @@ def my_is_partition(dev): "TYPE=iso9660": ["/dev/vdb"], "LABEL=config-2": ["/dev/vdb"], } - self.assertEqual(["/dev/vdb"], ds.find_candidate_devs()) + assert ["/dev/vdb"] == ds.find_candidate_devs() # add a vfat item # zdd reverse sorts after vdb, but config-2 label is preferred devs_with_answers["TYPE=vfat"] = ["/dev/zdd"] - self.assertEqual( - ["/dev/vdb", "/dev/zdd"], ds.find_candidate_devs() - ) + assert ["/dev/vdb", "/dev/zdd"] == ds.find_candidate_devs() # verify that partitions are considered, that have correct label. devs_with_answers = { @@ -630,7 +622,7 @@ def my_is_partition(dev): "TYPE=iso9660": [], "LABEL=config-2": ["/dev/vdb3"], } - self.assertEqual(["/dev/vdb3"], ds.find_candidate_devs()) + assert ["/dev/vdb3"] == ds.find_candidate_devs() # Verify that uppercase labels are also found. devs_with_answers = { @@ -638,22 +630,22 @@ def my_is_partition(dev): "TYPE=iso9660": ["/dev/vdb"], "LABEL=CONFIG-2": ["/dev/vdb"], } - self.assertEqual(["/dev/vdb"], ds.find_candidate_devs()) + assert ["/dev/vdb"] == ds.find_candidate_devs() finally: util.find_devs_with = orig_find_devs_with util.is_partition = orig_is_partition @mock.patch(M_PATH + "on_first_boot") - def test_pubkeys_v2(self, on_first_boot): + def test_pubkeys_v2(self, on_first_boot, tmp_path): """Verify that public-keys work in config-drive-v2.""" - myds = cfg_ds_from_dir(self.tmp, files=CFG_DRIVE_FILES_V2) - self.assertEqual( - myds.get_public_ssh_keys(), [OSTACK_META["public_keys"]["mykey"]] - ) - self.assertEqual("configdrive", myds.cloud_name) - self.assertEqual("openstack", myds.platform) - self.assertEqual("seed-dir (%s/seed)" % self.tmp, myds.subplatform) + myds = cfg_ds_from_dir(tmp_path, files=CFG_DRIVE_FILES_V2) + assert myds.get_public_ssh_keys() == [ + OSTACK_META["public_keys"]["mykey"] + ] + assert "configdrive" == myds.cloud_name + assert "openstack" == myds.platform + assert "seed-dir (%s/seed)" % tmp_path == myds.subplatform def test_subplatform_config_drive_when_starts_with_dev(self): """subplatform reports config-drive when source starts with /dev/.""" @@ -664,34 +656,29 @@ def test_subplatform_config_drive_when_starts_with_dev(self): with mock.patch(M_PATH + "util.mount_cb"): with mock.patch(M_PATH + "on_first_boot"): m_find_devs.return_value = ["/dev/anything"] - self.assertEqual(True, cfg_ds.get_data()) - self.assertEqual("config-disk (/dev/anything)", cfg_ds.subplatform) + assert True is cfg_ds.get_data() + assert "config-disk (/dev/anything)" == cfg_ds.subplatform @mock.patch( "cloudinit.net.is_openvswitch_internal_interface", mock.Mock(return_value=False), ) -class TestNetJson(CiTestCase): - def setUp(self): - super(TestNetJson, self).setUp() - self.tmp = self.tmp_dir() - self.maxDiff = None - +class TestNetJson: @mock.patch(M_PATH + "on_first_boot") - def test_network_data_is_found(self, on_first_boot): + def test_network_data_is_found(self, on_first_boot, tmp_path): """Verify that network_data is present in ds in config-drive-v2.""" - myds = cfg_ds_from_dir(self.tmp, files=CFG_DRIVE_FILES_V2) - self.assertIsNotNone(myds.network_json) + myds = cfg_ds_from_dir(tmp_path, files=CFG_DRIVE_FILES_V2) + assert myds.network_json is not None @mock.patch(M_PATH + "on_first_boot") - def test_network_config_is_converted(self, on_first_boot): + def test_network_config_is_converted(self, on_first_boot, tmp_path): """Verify that network_data is converted and present on ds object.""" - myds = cfg_ds_from_dir(self.tmp, files=CFG_DRIVE_FILES_V2) + myds = cfg_ds_from_dir(tmp_path, files=CFG_DRIVE_FILES_V2) network_config = openstack.convert_net_json( NETWORK_DATA, known_macs=KNOWN_MACS ) - self.assertEqual(myds.network_config, network_config) + assert myds.network_config == network_config def test_network_config_conversion_dhcp6(self): """Test some ipv6 input network json and check the expected @@ -749,7 +736,7 @@ def test_network_config_conversion_dhcp6(self): ], } conv_data = openstack.convert_net_json(in_data, known_macs=KNOWN_MACS) - self.assertEqual(out_data, conv_data) + assert out_data == conv_data def test_network_config_conversions(self): """Tests a bunch of input network json and checks the @@ -856,21 +843,14 @@ def test_network_config_conversions(self): conv_data = openstack.convert_net_json( in_data, known_macs=KNOWN_MACS ) - self.assertEqual(out_data, conv_data) + assert out_data == conv_data @mock.patch( "cloudinit.net.is_openvswitch_internal_interface", mock.Mock(return_value=False), ) -class TestConvertNetworkData(CiTestCase): - - with_logs = True - - def setUp(self): - super(TestConvertNetworkData, self).setUp() - self.tmp = self.tmp_dir() - +class TestConvertNetworkData: def _getnames_in_config(self, ncfg): return set( [n["name"] for n in ncfg["config"] if n["type"] == "physical"] @@ -880,7 +860,7 @@ def test_conversion_fills_names(self): ncfg = openstack.convert_net_json(NETWORK_DATA, known_macs=KNOWN_MACS) expected = set(["nic0", "enp0s1", "enp0s2"]) found = self._getnames_in_config(ncfg) - self.assertEqual(found, expected) + assert found == expected @mock.patch("cloudinit.net.get_interfaces_by_mac") def test_convert_reads_system_prefers_name(self, get_interfaces_by_mac): @@ -893,21 +873,20 @@ def test_convert_reads_system_prefers_name(self, get_interfaces_by_mac): ncfg = openstack.convert_net_json(NETWORK_DATA) expected = set(["nic0", "ens1", "enp0s2"]) found = self._getnames_in_config(ncfg) - self.assertEqual(found, expected) + assert found == expected def test_convert_raises_value_error_on_missing_name(self): macs = {"aa:aa:aa:aa:aa:00": "ens1"} with mock.patch( "cloudinit.sources.helpers.openstack.util.udevadm_settle" ): - self.assertRaises( - ValueError, - openstack.convert_net_json, - NETWORK_DATA, - known_macs=macs, - ) + with pytest.raises(ValueError): + openstack.convert_net_json( + NETWORK_DATA, + known_macs=macs, + ) - def test_conversion_with_route(self): + def test_conversion_with_route(self, tmp_path): ncfg = openstack.convert_net_json( NETWORK_DATA_2, known_macs=KNOWN_MACS ) @@ -917,19 +896,20 @@ def test_conversion_with_route(self): for n in ncfg["config"]: for s in n.get("subnets", []): routes.extend(s.get("routes", [])) - self.assertIn( - {"network": "0.0.0.0", "netmask": "0.0.0.0", "gateway": "2.2.2.9"}, - routes, - ) + assert { + "network": "0.0.0.0", + "netmask": "0.0.0.0", + "gateway": "2.2.2.9", + } in routes eni_renderer = eni.Renderer() eni_renderer.render_network_state( - network_state.parse_net_config_data(ncfg), target=self.tmp + network_state.parse_net_config_data(ncfg), target=str(tmp_path) ) with open( - os.path.join(self.tmp, "etc", "network", "interfaces"), "r" + os.path.join(tmp_path, "etc", "network", "interfaces"), "r" ) as f: eni_rendering = f.read() - self.assertIn("route add default gw 2.2.2.9", eni_rendering) + assert "route add default gw 2.2.2.9" in eni_rendering def test_conversion_with_tap(self): ncfg = openstack.convert_net_json( @@ -939,9 +919,9 @@ def test_conversion_with_tap(self): for i in ncfg["config"]: if i.get("type") == "physical": physicals.add(i["name"]) - self.assertEqual(physicals, set(("foo1", "foo2"))) + assert physicals == set(("foo1", "foo2")) - def test_bond_conversion(self): + def test_bond_conversion(self, tmp_path): # light testing of bond conversion and eni rendering of bond ncfg = openstack.convert_net_json( NETWORK_DATA_BOND, known_macs=KNOWN_MACS @@ -949,10 +929,10 @@ def test_bond_conversion(self): eni_renderer = eni.Renderer() eni_renderer.render_network_state( - network_state.parse_net_config_data(ncfg), target=self.tmp + network_state.parse_net_config_data(ncfg), target=str(tmp_path) ) with open( - os.path.join(self.tmp, "etc", "network", "interfaces"), "r" + os.path.join(tmp_path, "etc", "network", "interfaces"), "r" ) as f: eni_rendering = f.read() @@ -964,43 +944,43 @@ def test_bond_conversion(self): if i["type"] in ("vlan", "bond", "physical") ] ) - self.assertEqual( - sorted(["oeth0", "oeth1", "bond0", "bond0.602", "bond0.612"]), - interfaces, + assert ( + sorted(["oeth0", "oeth1", "bond0", "bond0.602", "bond0.612"]) + == interfaces ) words = eni_rendering.split() # 'eth0' and 'eth1' are the ids. because their mac adresses # map to other names, we should not see them in the ENI - self.assertNotIn("eth0", words) - self.assertNotIn("eth1", words) + assert "eth0" not in words + assert "eth1" not in words # oeth0 and oeth1 are the interface names for eni. # bond0 will be generated for the bond. Each should be auto. - self.assertIn("auto oeth0", eni_rendering) - self.assertIn("auto oeth1", eni_rendering) - self.assertIn("auto bond0", eni_rendering) + assert "auto oeth0" in eni_rendering + assert "auto oeth1" in eni_rendering + assert "auto bond0" in eni_rendering # The bond should have the given mac address pos = eni_rendering.find("auto bond0") - self.assertIn(BOND_MAC, eni_rendering[pos:]) + assert BOND_MAC in eni_rendering[pos:] - def test_vlan(self): + def test_vlan(self, tmp_path): # light testing of vlan config conversion and eni rendering ncfg = openstack.convert_net_json( NETWORK_DATA_VLAN, known_macs=KNOWN_MACS ) eni_renderer = eni.Renderer() eni_renderer.render_network_state( - network_state.parse_net_config_data(ncfg), target=self.tmp + network_state.parse_net_config_data(ncfg), target=str(tmp_path) ) with open( - os.path.join(self.tmp, "etc", "network", "interfaces"), "r" + os.path.join(tmp_path, "etc", "network", "interfaces"), "r" ) as f: eni_rendering = f.read() - self.assertIn("iface enp0s1", eni_rendering) - self.assertIn("address 10.0.1.5", eni_rendering) - self.assertIn("auto enp0s1.602", eni_rendering) + assert "iface enp0s1" in eni_rendering + assert "address 10.0.1.5" in eni_rendering + assert "auto enp0s1.602" in eni_rendering def test_mac_addrs_can_be_upper_case(self): # input mac addresses on rackspace may be upper case @@ -1019,9 +999,9 @@ def test_mac_addrs_can_be_upper_case(self): "enp0s1": "fa:16:3e:69:b0:58", "enp0s2": "fa:16:3e:d4:57:ad", } - self.assertEqual(expected, config_name2mac) + assert expected == config_name2mac - def test_unknown_device_types_accepted(self): + def test_unknown_device_types_accepted(self, caplog): # If we don't recognise a link, we should treat it as physical for a # best-effort boot my_netdata = deepcopy(NETWORK_DATA) @@ -1038,12 +1018,12 @@ def test_unknown_device_types_accepted(self): "enp0s1": "fa:16:3e:69:b0:58", "enp0s2": "fa:16:3e:d4:57:ad", } - self.assertEqual(expected, config_name2mac) + assert expected == config_name2mac # We should, however, warn the user that we don't recognise the type - self.assertIn( - "Unknown network_data link type (my-special-link-type)", - self.logs.getvalue(), + assert ( + "Unknown network_data link type (my-special-link-type)" + in caplog.text ) diff --git a/tests/unittests/sources/test_digitalocean.py b/tests/unittests/sources/test_digitalocean.py index c111e710ffc..0ef2e51a96a 100644 --- a/tests/unittests/sources/test_digitalocean.py +++ b/tests/unittests/sources/test_digitalocean.py @@ -8,10 +8,12 @@ import json -from cloudinit import helpers, settings +import pytest + +from cloudinit import settings from cloudinit.sources import DataSourceDigitalOcean from cloudinit.sources.helpers import digitalocean -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock DO_MULTIPLE_KEYS = [ "ssh-rsa AAAAB3NzaC1yc2EAAAA... test1@do.co", @@ -139,36 +141,39 @@ def _mock_dmi(): return (True, DO_META.get("id")) -class TestDataSourceDigitalOcean(CiTestCase): - """ - Test reading the meta-data - """ - - def setUp(self): - super(TestDataSourceDigitalOcean, self).setUp() - self.tmp = self.tmp_dir() - - def get_ds(self, get_sysinfo=_mock_dmi): +@pytest.fixture +def get_ds(paths): + def _get_ds(get_sysinfo=_mock_dmi): ds = DataSourceDigitalOcean.DataSourceDigitalOcean( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": self.tmp}) + settings.CFG_BUILTIN, None, paths ) ds.use_ip4LL = False if get_sysinfo is not None: ds._get_sysinfo = get_sysinfo return ds + return _get_ds + + +class TestDataSourceDigitalOcean: + """ + Test reading the meta-data + """ + @mock.patch("cloudinit.sources.helpers.digitalocean.read_sysinfo") - def test_returns_false_not_on_docean(self, m_read_sysinfo): + def test_returns_false_not_on_docean(self, m_read_sysinfo, get_ds): m_read_sysinfo.return_value = (False, None) - ds = self.get_ds(get_sysinfo=None) - self.assertEqual(False, ds.get_data()) - self.assertTrue(m_read_sysinfo.called) + ds = get_ds(get_sysinfo=None) + assert False is ds.get_data() + m_read_sysinfo.assert_called_once() @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") @mock.patch("cloudinit.sources.lifecycle.deprecate") - def test_deprecation_log_on_init(self, mock_deprecate, _mock_readmd): - ds = self.get_ds() - self.assertTrue(ds.get_data()) + def test_deprecation_log_on_init( + self, mock_deprecate, _mock_readmd, get_ds + ): + ds = get_ds() + assert ds.get_data() mock_deprecate.assert_called_with( deprecated="DataSourceDigitalOcean", deprecated_version="23.2", @@ -177,11 +182,13 @@ def test_deprecation_log_on_init(self, mock_deprecate, _mock_readmd): @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") @mock.patch("cloudinit.sources.lifecycle.deprecate") - def test_deprecation_log_on_unpick(self, mock_deprecate, _mock_readmd): - ds = self.get_ds() - self.assertTrue(ds.get_data()) + def test_deprecation_log_on_unpick( + self, mock_deprecate, _mock_readmd, get_ds + ): + ds = get_ds() + assert ds.get_data() ds._unpickle(0) - self.assertEqual(mock_deprecate.call_count, 2) + assert mock_deprecate.call_count == 2 mock_deprecate.assert_has_calls( [ mock.call( @@ -202,46 +209,44 @@ def test_deprecation_log_on_unpick(self, mock_deprecate, _mock_readmd): ) @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") - def test_metadata(self, mock_readmd): + def test_metadata(self, mock_readmd, get_ds): mock_readmd.return_value = DO_META.copy() - ds = self.get_ds() + ds = get_ds() ret = ds.get_data() - self.assertTrue(ret) + assert ret - self.assertTrue(mock_readmd.called) + mock_readmd.assert_called_once() - self.assertEqual(DO_META.get("user_data"), ds.get_userdata_raw()) - self.assertEqual(DO_META.get("vendor_data"), ds.get_vendordata_raw()) - self.assertEqual(DO_META.get("region"), ds.availability_zone) - self.assertEqual(DO_META.get("droplet_id"), ds.get_instance_id()) - self.assertEqual(DO_META.get("hostname"), ds.get_hostname().hostname) + assert DO_META.get("user_data") == ds.get_userdata_raw() + assert DO_META.get("vendor_data") == ds.get_vendordata_raw() + assert DO_META.get("region") == ds.availability_zone + assert DO_META.get("droplet_id") == ds.get_instance_id() + assert DO_META.get("hostname") == ds.get_hostname().hostname # Single key - self.assertEqual( - [DO_META.get("public_keys")], ds.get_public_ssh_keys() - ) + assert [DO_META.get("public_keys")] == ds.get_public_ssh_keys() - self.assertIsInstance(ds.get_public_ssh_keys(), list) + assert isinstance(ds.get_public_ssh_keys(), list) @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") - def test_multiple_ssh_keys(self, mock_readmd): + def test_multiple_ssh_keys(self, mock_readmd, get_ds): metadata = DO_META.copy() metadata["public_keys"] = DO_MULTIPLE_KEYS mock_readmd.return_value = metadata.copy() - ds = self.get_ds() + ds = get_ds() ret = ds.get_data() - self.assertTrue(ret) + assert ret is True - self.assertTrue(mock_readmd.called) + mock_readmd.assert_called_once() # Multiple keys - self.assertEqual(metadata["public_keys"], ds.get_public_ssh_keys()) - self.assertIsInstance(ds.get_public_ssh_keys(), list) + assert metadata["public_keys"] == ds.get_public_ssh_keys() + assert isinstance(ds.get_public_ssh_keys(), list) -class TestNetworkConvert(CiTestCase): +class TestNetworkConvert: def _get_networking(self): self.m_get_by_mac.return_value = { "04:01:57:d1:9e:01": "ens1", @@ -252,16 +257,16 @@ def _get_networking(self): netcfg = digitalocean.convert_network_configuration( DO_META["interfaces"], DO_META["dns"]["nameservers"] ) - self.assertIn("config", netcfg) + assert "config" in netcfg return netcfg - def setUp(self): - super(TestNetworkConvert, self).setUp() - self.add_patch("cloudinit.net.get_interfaces_by_mac", "m_get_by_mac") + @pytest.fixture(autouse=True) + def fix(self, mocker): + self.m_get_by_mac = mocker.patch("cloudinit.net.get_interfaces_by_mac") def test_networking_defined(self): netcfg = self._get_networking() - self.assertIsNotNone(netcfg) + assert netcfg is not None dns_defined = False for part in netcfg.get("config"): @@ -270,12 +275,12 @@ def test_networking_defined(self): if n_type == "nameserver": n_address = part.get("address") - self.assertIsNotNone(n_address) - self.assertEqual(len(n_address), 3) + assert n_address is not None + assert len(n_address) == 3 dns_resolvers = DO_META["dns"]["nameservers"] for x in n_address: - self.assertIn(x, dns_resolvers) + assert x in dns_resolvers dns_defined = True else: @@ -283,12 +288,12 @@ def test_networking_defined(self): n_name = part.get("name") n_mac = part.get("mac_address") - self.assertIsNotNone(n_type) - self.assertIsNotNone(n_subnets) - self.assertIsNotNone(n_name) - self.assertIsNotNone(n_mac) + assert n_type is not None + assert n_subnets is not None + assert n_name is not None + assert n_mac is not None - self.assertTrue(dns_defined) + assert dns_defined def _get_nic_definition(self, int_type, expected_name): """helper function to return if_type (i.e. public) and the expected @@ -296,7 +301,7 @@ def _get_nic_definition(self, int_type, expected_name): netcfg = self._get_networking() meta_def = (DO_META.get("interfaces")).get(int_type)[0] - self.assertEqual(int_type, meta_def.get("type")) + assert int_type == meta_def.get("type") for nic_def in netcfg.get("config"): print(nic_def) @@ -307,7 +312,7 @@ def _get_match_subn(self, subnets, ip_addr): """get the matching subnet definition based on ip address""" for subn in subnets: address = subn.get("address") - self.assertIsNotNone(address) + assert address is not None # equals won't work because of ipv6 addressing being in # cidr notation, i.e fe00::1/64 @@ -327,36 +332,36 @@ def test_correct_gateways_defined(self): gateways.append(subn.get("gateway")) # we should have two gateways, one ipv4 and ipv6 - self.assertEqual(len(gateways), 2) + assert len(gateways) == 2 # make that the ipv6 gateway is there (nic_def, meta_def) = self._get_nic_definition("public", "eth0") ipv4_def = meta_def.get("ipv4") - self.assertIn(ipv4_def.get("gateway"), gateways) + assert ipv4_def.get("gateway") in gateways # make sure the the ipv6 gateway is there ipv6_def = meta_def.get("ipv6") - self.assertIn(ipv6_def.get("gateway"), gateways) + assert ipv6_def.get("gateway") in gateways def test_public_interface_defined(self): """test that the public interface is defined as eth0""" (nic_def, meta_def) = self._get_nic_definition("public", "eth0") - self.assertEqual("eth0", nic_def.get("name")) - self.assertEqual(meta_def.get("mac"), nic_def.get("mac_address")) - self.assertEqual("physical", nic_def.get("type")) + assert "eth0" == nic_def.get("name") + assert meta_def.get("mac") == nic_def.get("mac_address") + assert "physical" == nic_def.get("type") def test_private_interface_defined(self): """test that the private interface is defined as eth1""" (nic_def, meta_def) = self._get_nic_definition("private", "eth1") - self.assertEqual("eth1", nic_def.get("name")) - self.assertEqual(meta_def.get("mac"), nic_def.get("mac_address")) - self.assertEqual("physical", nic_def.get("type")) + assert "eth1" == nic_def.get("name") + assert meta_def.get("mac") == nic_def.get("mac_address") + assert "physical" == nic_def.get("type") def test_public_interface_ipv6(self): """test public ipv6 addressing""" (nic_def, meta_def) = self._get_nic_definition("public", "eth0") ipv6_def = meta_def.get("ipv6") - self.assertIsNotNone(ipv6_def) + assert ipv6_def is not None subn_def = self._get_match_subn( nic_def.get("subnets"), ipv6_def.get("ip_address") @@ -366,34 +371,34 @@ def test_public_interface_ipv6(self): ipv6_def.get("ip_address"), ipv6_def.get("cidr") ) - self.assertEqual(cidr_notated_address, subn_def.get("address")) - self.assertEqual(ipv6_def.get("gateway"), subn_def.get("gateway")) + assert cidr_notated_address == subn_def.get("address") + assert ipv6_def.get("gateway") == subn_def.get("gateway") def test_public_interface_ipv4(self): """test public ipv4 addressing""" (nic_def, meta_def) = self._get_nic_definition("public", "eth0") ipv4_def = meta_def.get("ipv4") - self.assertIsNotNone(ipv4_def) + assert ipv4_def is not None subn_def = self._get_match_subn( nic_def.get("subnets"), ipv4_def.get("ip_address") ) - self.assertEqual(ipv4_def.get("netmask"), subn_def.get("netmask")) - self.assertEqual(ipv4_def.get("gateway"), subn_def.get("gateway")) + assert ipv4_def.get("netmask") == subn_def.get("netmask") + assert ipv4_def.get("gateway") == subn_def.get("gateway") def test_public_interface_anchor_ipv4(self): """test public ipv4 addressing""" (nic_def, meta_def) = self._get_nic_definition("public", "eth0") ipv4_def = meta_def.get("anchor_ipv4") - self.assertIsNotNone(ipv4_def) + assert ipv4_def is not None subn_def = self._get_match_subn( nic_def.get("subnets"), ipv4_def.get("ip_address") ) - self.assertEqual(ipv4_def.get("netmask"), subn_def.get("netmask")) - self.assertNotIn("gateway", subn_def) + assert ipv4_def.get("netmask") == subn_def.get("netmask") + assert "gateway" not in subn_def @mock.patch("cloudinit.net.get_interfaces_by_mac") def test_convert_without_private(self, m_get_by_mac): @@ -414,10 +419,9 @@ def test_convert_without_private(self, m_get_by_mac): "name '%s' in config twice: %s" % (i["name"], netcfg) ) byname[i["name"]] = i - self.assertTrue("eth0" in byname) - self.assertTrue("subnets" in byname["eth0"]) + assert "eth0" in byname + assert "subnets" in byname["eth0"] eth0 = byname["eth0"] - self.assertEqual( - sorted(["45.55.249.133", "10.17.0.5"]), - sorted([i["address"] for i in eth0["subnets"]]), + assert sorted(["45.55.249.133", "10.17.0.5"]) == sorted( + [i["address"] for i in eth0["subnets"]] ) From af8bb5f974d4d06d1e063d48d9fa6d82df935859 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 6 Aug 2025 16:25:56 +0200 Subject: [PATCH 057/150] test: drop citestcase from g and h sources (#6344) --- tests/unittests/sources/test_gce.py | 106 ++++++++++-------------- tests/unittests/sources/test_hetzner.py | 45 +++++----- 2 files changed, 64 insertions(+), 87 deletions(-) diff --git a/tests/unittests/sources/test_gce.py b/tests/unittests/sources/test_gce.py index 729c12df0d7..d1e60472eaf 100644 --- a/tests/unittests/sources/test_gce.py +++ b/tests/unittests/sources/test_gce.py @@ -11,12 +11,12 @@ from unittest import mock from urllib.parse import urlparse +import pytest import responses from cloudinit import distros, helpers, settings from cloudinit.net.dhcp import NoDHCPLeaseError from cloudinit.sources import DataSourceGCE -from tests.unittests import helpers as test_helpers M_PATH = "cloudinit.sources.DataSourceGCE." @@ -61,7 +61,7 @@ ) -class TestDataSourceGCE(test_helpers.CiTestCase): +class TestDataSourceGCE: with_logs = True def _make_distro(self, dtype, def_user=None): @@ -74,27 +74,18 @@ def _make_distro(self, dtype, def_user=None): distro = distro_cls(dtype, cfg["system_info"], paths) return distro - def setUp(self): - tmp = self.tmp_dir() + @pytest.fixture(autouse=True) + def fixtures(self, mocker, paths): self.ds = DataSourceGCE.DataSourceGCE( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": tmp}) + settings.CFG_BUILTIN, None, paths ) - - ppatch = self.m_platform_reports_gce = mock.patch( - M_PATH + "platform_reports_gce" + self.m_platform_reports_gce = mocker.patch( + M_PATH + "platform_reports_gce", return_value=True ) - self.m_platform_reports_gce = ppatch.start() - self.m_platform_reports_gce.return_value = True - self.addCleanup(ppatch.stop) - - pppatch = self.m_is_resolvable_url = mock.patch( + self.m_is_resolvable_url = mocker.patch( M_PATH + "util.is_resolvable_url", return_value=True ) - self.m_is_resolvable_url = pppatch.start() - self.addCleanup(pppatch.stop) - - self.add_patch("time.sleep", "m_sleep") # just to speed up tests - super(TestDataSourceGCE, self).setUp() + mocker.patch("time.sleep") def _set_mock_metadata(self, gce_meta=None, *, check_headers=None): if gce_meta is None: @@ -115,7 +106,7 @@ def _request_callback(request): response = json.dumps(response) if check_headers is not None: for k in check_headers.keys(): - self.assertEqual(check_headers[k], request.headers[k]) + assert check_headers[k] == request.headers[k] return (200, request.headers, response) else: return (404, request.headers, "") @@ -130,7 +121,7 @@ def _request_callback(request): def test_connection(self): self._set_mock_metadata(check_headers=HEADERS) success = self.ds.get_data() - self.assertTrue(success) + assert success @responses.activate def test_metadata(self): @@ -142,15 +133,13 @@ def test_metadata(self): self.ds.get_data() shostname = GCE_META.get("instance/hostname").split(".")[0] - self.assertEqual(shostname, self.ds.get_hostname().hostname) + assert shostname == self.ds.get_hostname().hostname - self.assertEqual( - GCE_META.get("instance/id"), self.ds.get_instance_id() - ) + assert GCE_META.get("instance/id") == self.ds.get_instance_id() - self.assertEqual( - GCE_META.get("instance/attributes/user-data"), - self.ds.get_userdata_raw(), + assert ( + GCE_META.get("instance/attributes/user-data") + == self.ds.get_userdata_raw() ) @responses.activate @@ -159,21 +148,19 @@ def test_metadata_partial(self): self._set_mock_metadata(GCE_META_PARTIAL) self.ds.get_data() - self.assertEqual( - GCE_META_PARTIAL.get("instance/id"), self.ds.get_instance_id() - ) + assert GCE_META_PARTIAL.get("instance/id") == self.ds.get_instance_id() shostname = GCE_META_PARTIAL.get("instance/hostname").split(".")[0] - self.assertEqual(shostname, self.ds.get_hostname().hostname) + assert shostname == self.ds.get_hostname().hostname @responses.activate def test_userdata_no_encoding(self): """check that user-data is read.""" self._set_mock_metadata(GCE_USER_DATA_TEXT) self.ds.get_data() - self.assertEqual( - GCE_USER_DATA_TEXT["instance/attributes"]["user-data"].encode(), - self.ds.get_userdata_raw(), + assert ( + GCE_USER_DATA_TEXT["instance/attributes"]["user-data"].encode() + == self.ds.get_userdata_raw() ) @responses.activate @@ -184,7 +171,7 @@ def test_metadata_encoding(self): instance_data = GCE_META_ENCODING.get("instance/attributes") decoded = b64decode(instance_data.get("user-data")) - self.assertEqual(decoded, self.ds.get_userdata_raw()) + assert decoded == self.ds.get_userdata_raw() @responses.activate def test_missing_required_keys_return_false(self): @@ -196,14 +183,14 @@ def test_missing_required_keys_return_false(self): meta = GCE_META_PARTIAL.copy() del meta[required_key] self._set_mock_metadata(meta) - self.assertEqual(False, self.ds.get_data()) + assert False is self.ds.get_data() responses.reset() @responses.activate def test_no_ssh_keys_metadata(self): self._set_mock_metadata() self.ds.get_data() - self.assertEqual([], self.ds.get_public_ssh_keys()) + assert [] == self.ds.get_public_ssh_keys() @responses.activate def test_cloudinit_ssh_keys(self): @@ -241,17 +228,17 @@ def test_cloudinit_ssh_keys(self): self.ds.get_data() expected = [valid_key.format(key) for key in range(3)] - self.assertEqual(set(expected), set(self.ds.get_public_ssh_keys())) + assert set(expected) == set(self.ds.get_public_ssh_keys()) @responses.activate @mock.patch(M_PATH + "ug_util") - def test_default_user_ssh_keys(self, mock_ug_util): + def test_default_user_ssh_keys(self, mock_ug_util, paths): mock_ug_util.normalize_users_groups.return_value = None, None mock_ug_util.extract_default.return_value = "ubuntu", None ubuntu_ds = DataSourceGCE.DataSourceGCE( settings.CFG_BUILTIN, self._make_distro("ubuntu"), - helpers.Paths({"run_dir": self.tmp_dir()}), + paths, ) valid_key = "ssh-rsa VALID {0}" @@ -288,7 +275,7 @@ def test_default_user_ssh_keys(self, mock_ug_util): ubuntu_ds.get_data() expected = [valid_key.format(key) for key in range(3)] - self.assertEqual(set(expected), set(ubuntu_ds.get_public_ssh_keys())) + assert set(expected) == set(ubuntu_ds.get_public_ssh_keys()) @responses.activate def test_instance_ssh_keys_override(self): @@ -312,7 +299,7 @@ def test_instance_ssh_keys_override(self): self.ds.get_data() expected = [valid_key.format(key) for key in range(2)] - self.assertEqual(set(expected), set(self.ds.get_public_ssh_keys())) + assert set(expected) == set(self.ds.get_public_ssh_keys()) @responses.activate def test_block_project_ssh_keys_override(self): @@ -335,20 +322,19 @@ def test_block_project_ssh_keys_override(self): self.ds.get_data() expected = [valid_key.format(0)] - self.assertEqual(set(expected), set(self.ds.get_public_ssh_keys())) + assert set(expected) == set(self.ds.get_public_ssh_keys()) @responses.activate def test_only_last_part_of_zone_used_for_availability_zone(self): self._set_mock_metadata() - r = self.ds.get_data() - self.assertEqual(True, r) - self.assertEqual("bar", self.ds.availability_zone) + assert True is self.ds.get_data() + assert "bar" == self.ds.availability_zone @mock.patch("cloudinit.sources.DataSourceGCE.GoogleMetadataFetcher") def test_get_data_returns_false_if_not_on_gce(self, m_fetcher): self.m_platform_reports_gce.return_value = False ret = self.ds.get_data() - self.assertEqual(False, ret) + assert False is ret m_fetcher.assert_not_called() def test_has_expired(self): @@ -375,7 +361,7 @@ def _get_timestamp(days): } for key, expired in ssh_keys.items(): - self.assertEqual(DataSourceGCE._has_expired(key), expired) + assert DataSourceGCE._has_expired(key) == expired def test_parse_public_keys_non_ascii(self): public_key_data = [ @@ -389,7 +375,7 @@ def test_parse_public_keys_non_ascii(self): found = DataSourceGCE._parse_public_keys( public_key_data, default_user="default" ) - self.assertEqual(sorted(found), sorted(expected)) + assert sorted(found) == sorted(expected) @mock.patch("cloudinit.url_helper.readurl") def test_publish_host_keys(self, m_readurl): @@ -420,11 +406,11 @@ def test_publish_host_keys(self, m_readurl): ) @mock.patch(M_PATH + "net.find_candidate_nics", return_value=["ens4"]) def test_local_datasource_uses_ephemeral_dhcp( - self, _m_find_candidate_nics, m_dhcp + self, _m_find_candidate_nics, m_dhcp, tmp_path ): self._set_mock_metadata() distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir + distro.get_tmp_exec_path = str(tmp_path) ds = DataSourceGCE.DataSourceGCELocal( sys_cfg={}, distro=distro, paths=None ) @@ -439,11 +425,11 @@ def test_local_datasource_uses_ephemeral_dhcp( ) @mock.patch(M_PATH + "net.find_candidate_nics") def test_local_datasource_tries_on_multi_nic( - self, m_find_candidate_nics, m_dhcp, m_read_md + self, m_find_candidate_nics, m_dhcp, m_read_md, caplog, tmp_path ): self._set_mock_metadata() distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir + distro.get_tmp_exec_path = str(tmp_path) ds = DataSourceGCE.DataSourceGCELocal( sys_cfg={}, distro=distro, paths=None ) @@ -488,10 +474,7 @@ def test_local_datasource_tries_on_multi_nic( " whoopsie, not this one", ) for msg in expected_logs: - self.assertIn( - msg, - self.logs.getvalue(), - ) + assert msg in caplog.text @responses.activate @mock.patch( @@ -511,11 +494,11 @@ def test_datasource_doesnt_use_ephemeral_dhcp(self, m_dhcp): ) @mock.patch(M_PATH + "net.find_candidate_nics") def test_datasource_on_dhcp_lease_failure( - self, m_find_candidate_nics, m_dhcp + self, m_find_candidate_nics, m_dhcp, caplog, tmp_path ): self._set_mock_metadata() distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir + distro.get_tmp_exec_path = str(tmp_path) ds = DataSourceGCE.DataSourceGCELocal( sys_cfg={}, distro=distro, paths=None ) @@ -539,10 +522,7 @@ def test_datasource_on_dhcp_lease_failure( "Unable to obtain a DHCP lease for ens0p5", ) for msg in expected_logs: - self.assertIn( - msg, - self.logs.getvalue(), - ) + assert msg in caplog.text @responses.activate def test_instance_and_project_data_decoded(self): diff --git a/tests/unittests/sources/test_hetzner.py b/tests/unittests/sources/test_hetzner.py index 5867a4fac91..268837562db 100644 --- a/tests/unittests/sources/test_hetzner.py +++ b/tests/unittests/sources/test_hetzner.py @@ -4,9 +4,11 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import helpers, settings, util +import pytest + +from cloudinit import settings, util from cloudinit.sources import DataSourceHetzner -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock METADATA = util.load_yaml( """ @@ -51,20 +53,17 @@ """ -class TestDataSourceHetzner(CiTestCase): +class TestDataSourceHetzner: """ Test reading the meta-data """ - def setUp(self): - super(TestDataSourceHetzner, self).setUp() - self.tmp = self.tmp_dir() - - def get_ds(self): + @pytest.fixture + def ds(self, paths, tmp_path): distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir + distro.get_tmp_exec_path = str(tmp_path) ds = DataSourceHetzner.DataSourceHetzner( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) + settings.CFG_BUILTIN, distro, paths ) return ds @@ -82,6 +81,7 @@ def test_read_data( m_fallback_nic, m_net, m_dhcp, + ds, ): m_get_hcloud_data.return_value = ( True, @@ -100,9 +100,7 @@ def test_read_data( } ] - ds = self.get_ds() - ret = ds.get_data() - self.assertTrue(ret) + assert True is ds.get_data() m_net.assert_called_once_with( ds.distro, @@ -114,29 +112,28 @@ def test_read_data( ], ) - self.assertTrue(m_readmd.called) + assert 0 != m_readmd.call_count - self.assertEqual(METADATA.get("hostname"), ds.get_hostname().hostname) + assert METADATA.get("hostname") == ds.get_hostname().hostname - self.assertEqual(METADATA.get("public-keys"), ds.get_public_ssh_keys()) + assert METADATA.get("public-keys") == ds.get_public_ssh_keys() - self.assertIsInstance(ds.get_public_ssh_keys(), list) - self.assertEqual(ds.get_userdata_raw(), USERDATA) - self.assertEqual(ds.get_vendordata_raw(), METADATA.get("vendor_data")) + assert isinstance(ds.get_public_ssh_keys(), list) + assert ds.get_userdata_raw() == USERDATA + assert ds.get_vendordata_raw() == METADATA.get("vendor_data") @mock.patch("cloudinit.sources.helpers.hetzner.read_metadata") @mock.patch("cloudinit.net.find_fallback_nic") @mock.patch("cloudinit.sources.DataSourceHetzner.get_hcloud_data") def test_not_on_hetzner_returns_false( - self, m_get_hcloud_data, m_find_fallback, m_read_md + self, m_get_hcloud_data, m_find_fallback, m_read_md, ds ): """If helper 'get_hcloud_data' returns False, return False from get_data.""" m_get_hcloud_data.return_value = (False, None) - ds = self.get_ds() ret = ds.get_data() - self.assertFalse(ret) + assert not ret # These are a white box attempt to ensure it did not search. - m_find_fallback.assert_not_called() - m_read_md.assert_not_called() + assert 0 == m_find_fallback.call_count + assert 0 == m_read_md.call_count From a15b77b1834f34f71a63642870a221865d755902 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 7 Aug 2025 11:41:28 +0200 Subject: [PATCH 058/150] test: drop citestcase from i sources (#6346) --- tests/unittests/sources/test_ibmcloud.py | 31 +- tests/unittests/sources/test_init.py | 589 ++++++++++------------- 2 files changed, 270 insertions(+), 350 deletions(-) diff --git a/tests/unittests/sources/test_ibmcloud.py b/tests/unittests/sources/test_ibmcloud.py index 7154703c147..2589871a5f8 100644 --- a/tests/unittests/sources/test_ibmcloud.py +++ b/tests/unittests/sources/test_ibmcloud.py @@ -8,7 +8,6 @@ import pytest from cloudinit import util -from cloudinit.helpers import Paths from cloudinit.sources import DataSourceIBMCloud as ibm from tests.unittests import helpers as test_helpers @@ -20,7 +19,7 @@ @mock.patch(D_PATH + "_is_xen", return_value=True) @mock.patch(D_PATH + "_is_ibm_provisioning") @mock.patch(D_PATH + "util.blkid") -class TestGetIBMPlatform(test_helpers.CiTestCase): +class TestGetIBMPlatform: """Test the get_ibm_platform helper.""" blkid_base = { @@ -57,7 +56,8 @@ class TestGetIBMPlatform(test_helpers.CiTestCase): } } - def setUp(self): + @pytest.fixture(autouse=True) + def fixtures(self): self.blkid_metadata = copy.deepcopy(self.blkid_base) self.blkid_metadata.update(copy.deepcopy(self.blkid_metadata_disk)) @@ -108,7 +108,7 @@ def test_id_os_code_must_match_uuid(self, m_blkid, m_is_prov, _m_xen): @mock.patch(D_PATH + "_read_system_uuid", return_value=None) @mock.patch(D_PATH + "get_ibm_platform") -class TestReadMD(test_helpers.CiTestCase): +class TestReadMD: """Test the read_datasource helper.""" template_md = { @@ -250,9 +250,9 @@ def test_provisioning_not_ibm(self, m_platform, m_sysuuid): m_platform.return_value = (None, None) assert ibm.read_md() is None - def test_template_live(self, m_platform, m_sysuuid): + def test_template_live(self, m_platform, m_sysuuid, tmp_path): """Template live environment should be identified.""" - tmpdir = self.tmp_dir() + tmpdir = str(tmp_path) m_platform.return_value = ( ibm.Platforms.TEMPLATE_LIVE_METADATA, tmpdir, @@ -280,9 +280,9 @@ def test_template_live(self, m_platform, m_sysuuid): assert self._get_expected_metadata(self.template_md) == ret["metadata"] assert self.sysuuid == ret["system-uuid"] - def test_os_code_live(self, m_platform, m_sysuuid): + def test_os_code_live(self, m_platform, m_sysuuid, tmp_path): """Verify an os_code metadata path.""" - tmpdir = self.tmp_dir() + tmpdir = str(tmp_path) m_platform.return_value = (ibm.Platforms.OS_CODE, tmpdir) netdata = json.dumps(self.network_data) test_helpers.populate_dir( @@ -305,9 +305,9 @@ def test_os_code_live(self, m_platform, m_sysuuid): assert self.userdata == ret["userdata"] assert self._get_expected_metadata(self.oscode_md) == ret["metadata"] - def test_os_code_live_no_userdata(self, m_platform, m_sysuuid): + def test_os_code_live_no_userdata(self, m_platform, m_sysuuid, tmp_path): """Verify os_code without user-data.""" - tmpdir = self.tmp_dir() + tmpdir = str(tmp_path) m_platform.return_value = (ibm.Platforms.OS_CODE, tmpdir) test_helpers.populate_dir( tmpdir, @@ -377,13 +377,10 @@ def test_config_and_log_no_reference(self, caplog, tmp_path): assert "no reference file" in caplog.text -class TestDataSourceIBMCloud(test_helpers.CiTestCase): - def setUp(self): - super(TestDataSourceIBMCloud, self).setUp() - self.tmp = self.tmp_dir() - self.cloud_dir = self.tmp_path("cloud", dir=self.tmp) - util.ensure_dir(self.cloud_dir) - paths = Paths({"run_dir": self.tmp, "cloud_dir": self.cloud_dir}) +class TestDataSourceIBMCloud: + @pytest.fixture(autouse=True) + def fixture(self, paths): + util.ensure_dir(paths.cloud_dir) self.ds = ibm.DataSourceIBMCloud(sys_cfg={}, distro=None, paths=paths) def test_get_data_false(self): diff --git a/tests/unittests/sources/test_init.py b/tests/unittests/sources/test_init.py index de8ded4a32f..05bebd6e0d9 100644 --- a/tests/unittests/sources/test_init.py +++ b/tests/unittests/sources/test_init.py @@ -3,9 +3,12 @@ import copy import inspect +import logging import os import stat +import pytest + from cloudinit import importer, util from cloudinit.distros import ubuntu from cloudinit.event import EventScope, EventType @@ -21,7 +24,7 @@ redact_sensitive_keys, ) from cloudinit.user_data import UserDataProcessor -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, assert_count_equal, mock class DataSourceTestSubclassNet(DataSource): @@ -67,61 +70,60 @@ class InvalidDataSourceTestSubclassNet(DataSource): pass -class TestDataSource(CiTestCase): - - with_logs = True - maxDiff = None +class TestDataSource: - def setUp(self): - super(TestDataSource, self).setUp() + @pytest.fixture(autouse=True) + def fixtures(self, paths): self.sys_cfg = {"datasource": {"_undef": {"key1": False}}} self.distro = ubuntu.Distro("somedistro", {}, {}) - self.paths = Paths({}) - self.datasource = DataSource(self.sys_cfg, self.distro, self.paths) + self.datasource = DataSource(self.sys_cfg, self.distro, paths) + + @pytest.fixture + def datasource(self, paths): + return DataSourceTestSubclassNet(self.sys_cfg, self.distro, paths) def test_datasource_init(self): """DataSource initializes metadata attributes, ds_cfg and ud_proc.""" - self.assertEqual(self.paths, self.datasource.paths) - self.assertEqual(self.sys_cfg, self.datasource.sys_cfg) - self.assertEqual(self.distro, self.datasource.distro) - self.assertIsNone(self.datasource.userdata) - self.assertEqual({}, self.datasource.metadata) - self.assertIsNone(self.datasource.userdata_raw) - self.assertIsNone(self.datasource.vendordata) - self.assertIsNone(self.datasource.vendordata_raw) - self.assertEqual({"key1": False}, self.datasource.ds_cfg) - self.assertIsInstance(self.datasource.ud_proc, UserDataProcessor) - - def test_datasource_init_gets_ds_cfg_using_dsname(self): + assert self.sys_cfg == self.datasource.sys_cfg + assert self.distro == self.datasource.distro + assert self.datasource.userdata is None + assert {} == self.datasource.metadata + assert self.datasource.userdata_raw is None + assert self.datasource.vendordata is None + assert self.datasource.vendordata_raw is None + assert {"key1": False} == self.datasource.ds_cfg + assert isinstance(self.datasource.ud_proc, UserDataProcessor) + + def test_datasource_init_gets_ds_cfg_using_dsname(self, paths): """Init uses DataSource.dsname for sourcing ds_cfg.""" sys_cfg = {"datasource": {"MyTestSubclass": {"key2": False}}} distro = "distrotest" # generally should be a Distro object - datasource = DataSourceTestSubclassNet(sys_cfg, distro, self.paths) - self.assertEqual({"key2": False}, datasource.ds_cfg) + datasource = DataSourceTestSubclassNet(sys_cfg, distro, paths) + assert {"key2": False} == datasource.ds_cfg - def test_str_is_classname(self): + def test_str_is_classname(self, paths): """The string representation of the datasource is the classname.""" - self.assertEqual("DataSource", str(self.datasource)) - self.assertEqual( - "DataSourceTestSubclassNet", - str(DataSourceTestSubclassNet("", "", self.paths)), + assert "DataSource" == str(self.datasource) + assert "DataSourceTestSubclassNet" == str( + DataSourceTestSubclassNet("", "", paths) ) def test_datasource_get_url_params_defaults(self): """get_url_params default url config settings for the datasource.""" params = self.datasource.get_url_params() - self.assertEqual(params.max_wait_seconds, self.datasource.url_max_wait) - self.assertEqual(params.timeout_seconds, self.datasource.url_timeout) - self.assertEqual(params.num_retries, self.datasource.url_retries) - self.assertEqual( - params.sec_between_retries, self.datasource.url_sec_between_retries + assert params.max_wait_seconds == self.datasource.url_max_wait + assert params.timeout_seconds == self.datasource.url_timeout + assert params.num_retries == self.datasource.url_retries + assert ( + params.sec_between_retries + == self.datasource.url_sec_between_retries ) - def test_datasource_get_url_params_subclassed(self): + def test_datasource_get_url_params_subclassed(self, paths): """Subclasses can override get_url_params defaults.""" sys_cfg = {"datasource": {"MyTestSubclass": {"key2": False}}} distro = "distrotest" # generally should be a Distro object - datasource = DataSourceTestSubclassNet(sys_cfg, distro, self.paths) + datasource = DataSourceTestSubclassNet(sys_cfg, distro, paths) expected = ( datasource.url_max_wait, datasource.url_timeout, @@ -129,10 +131,10 @@ def test_datasource_get_url_params_subclassed(self): datasource.url_sec_between_retries, ) url_params = datasource.get_url_params() - self.assertNotEqual(self.datasource.get_url_params(), url_params) - self.assertEqual(expected, url_params) + assert self.datasource.get_url_params() != url_params + assert expected == url_params - def test_datasource_get_url_params_ds_config_override(self): + def test_datasource_get_url_params_ds_config_override(self, paths): """Datasource configuration options can override url param defaults.""" sys_cfg = { "datasource": { @@ -144,36 +146,31 @@ def test_datasource_get_url_params_ds_config_override(self): } } } - datasource = DataSourceTestSubclassNet( - sys_cfg, self.distro, self.paths - ) + datasource = DataSourceTestSubclassNet(sys_cfg, self.distro, paths) expected = (1, 2, 3, 4) url_params = datasource.get_url_params() - self.assertNotEqual( - ( - datasource.url_max_wait, - datasource.url_timeout, - datasource.url_retries, - datasource.url_sec_between_retries, - ), - url_params, - ) - self.assertEqual(expected, url_params) + assert ( + datasource.url_max_wait, + datasource.url_timeout, + datasource.url_retries, + datasource.url_sec_between_retries, + ) != url_params + assert expected == url_params - def test_datasource_get_url_params_is_zero_or_greater(self): + def test_datasource_get_url_params_is_zero_or_greater(self, paths): """get_url_params ignores timeouts with a value below 0.""" # Set an override that is below 0 which gets ignored. sys_cfg = {"datasource": {"_undef": {"timeout": "-1"}}} - datasource = DataSource(sys_cfg, self.distro, self.paths) + datasource = DataSource(sys_cfg, self.distro, paths) ( _max_wait, timeout, _retries, _sec_between_retries, ) = datasource.get_url_params() - self.assertEqual(0, timeout) + assert 0 == timeout - def test_datasource_get_url_uses_defaults_on_errors(self): + def test_datasource_get_url_uses_defaults_on_errors(self, caplog, paths): """On invalid system config values for url_params defaults are used.""" # All invalid values should be logged sys_cfg = { @@ -185,7 +182,7 @@ def test_datasource_get_url_uses_defaults_on_errors(self): } } } - datasource = DataSource(sys_cfg, self.distro, self.paths) + datasource = DataSource(sys_cfg, self.distro, paths) url_params = datasource.get_url_params() expected = ( datasource.url_max_wait, @@ -193,188 +190,158 @@ def test_datasource_get_url_uses_defaults_on_errors(self): datasource.url_retries, datasource.url_sec_between_retries, ) - self.assertEqual(expected, url_params) - logs = self.logs.getvalue() + assert expected == url_params expected_logs = [ "Config max_wait 'nope' is not an int, using default '-1'", "Config timeout 'bug' is not an int, using default '10'", "Config retries 'nonint' is not an int, using default '5'", ] for log in expected_logs: - self.assertIn(log, logs) + assert log in caplog.text @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" - self.assertEqual("nic9", self.datasource.distro.fallback_interface) + assert "nic9" == self.datasource.distro.fallback_interface @mock.patch("cloudinit.sources.net.find_fallback_nic") - def test_fallback_interface_logs_undiscovered(self, m_get_fallback_nic): + def test_fallback_interface_logs_undiscovered( + self, m_get_fallback_nic, caplog + ): """Log a warning when fallback_interface can not discover the nic.""" m_get_fallback_nic.return_value = None # Couldn't discover nic - self.assertIsNone(self.datasource.distro.fallback_interface) - self.assertEqual( - "WARNING: Did not find a fallback interface on distro: " - "somedistro.\n", - self.logs.getvalue(), - ) + assert self.datasource.distro.fallback_interface is None + assert ( + mock.ANY, + logging.WARNING, + "Did not find a fallback interface on distro: somedistro.", + ) in caplog.record_tuples @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.distro.fallback_interface = "nic10" - self.assertEqual("nic10", self.datasource.distro.fallback_interface) + assert "nic10" == self.datasource.distro.fallback_interface m_get_fallback_nic.assert_not_called() - def test__get_data_unimplemented(self): + def test__get_data_unimplemented(self, paths): """Raise an error when _get_data is not implemented.""" - with self.assertRaises(NotImplementedError) as context_manager: + with pytest.raises( + NotImplementedError, + match="Subclasses of DataSource must implement _get_data", + ): self.datasource.get_data() - self.assertIn( - "Subclasses of DataSource must implement _get_data", - str(context_manager.exception), - ) datasource2 = InvalidDataSourceTestSubclassNet( - self.sys_cfg, self.distro, self.paths + self.sys_cfg, self.distro, paths ) - with self.assertRaises(NotImplementedError) as context_manager: + with pytest.raises( + NotImplementedError, + match="Subclasses of DataSource must implement _get_data", + ): datasource2.get_data() - self.assertIn( - "Subclasses of DataSource must implement _get_data", - str(context_manager.exception), - ) - def test_get_data_calls_subclass__get_data(self): + def test_get_data_calls_subclass__get_data(self, datasource): """Datasource.get_data uses the subclass' version of _get_data.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) - self.assertTrue(datasource.get_data()) - self.assertEqual( - { - "availability_zone": "myaz", - "local-hostname": "test-subclass-hostname", - "region": "myregion", - }, - datasource.metadata, - ) - self.assertEqual("userdata_raw", datasource.userdata_raw) - self.assertEqual("vendordata_raw", datasource.vendordata_raw) - - def test_get_hostname_strips_local_hostname_without_domain(self): + assert datasource.get_data() is True + assert { + "availability_zone": "myaz", + "local-hostname": "test-subclass-hostname", + "region": "myregion", + } == datasource.metadata + assert "userdata_raw" == datasource.userdata_raw + assert "vendordata_raw" == datasource.vendordata_raw + + def test_get_hostname_strips_local_hostname_without_domain( + self, datasource + ): """Datasource.get_hostname strips metadata local-hostname of domain.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) - self.assertTrue(datasource.get_data()) - self.assertEqual( - "test-subclass-hostname", datasource.metadata["local-hostname"] - ) - self.assertEqual( - "test-subclass-hostname", datasource.get_hostname().hostname + assert datasource.get_data() + assert ( + "test-subclass-hostname" == datasource.metadata["local-hostname"] ) + assert "test-subclass-hostname" == datasource.get_hostname().hostname datasource.metadata["local-hostname"] = "hostname.my.domain.com" - self.assertEqual("hostname", datasource.get_hostname().hostname) + assert "hostname" == datasource.get_hostname().hostname - def test_get_hostname_with_fqdn_returns_local_hostname_with_domain(self): + def test_get_hostname_with_fqdn_returns_local_hostname_with_domain( + self, datasource + ): """Datasource.get_hostname with fqdn set gets qualified hostname.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) - self.assertTrue(datasource.get_data()) + assert datasource.get_data() datasource.metadata["local-hostname"] = "hostname.my.domain.com" - self.assertEqual( - "hostname.my.domain.com", - datasource.get_hostname(fqdn=True).hostname, + assert ( + "hostname.my.domain.com" + == datasource.get_hostname(fqdn=True).hostname ) - def test_get_hostname_without_metadata_uses_system_hostname(self): + def test_get_hostname_without_metadata_uses_system_hostname( + self, datasource + ): """Datasource.gethostname runs util.get_hostname when no metadata.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) - self.assertEqual({}, datasource.metadata) + assert {} == datasource.metadata mock_fqdn = "cloudinit.sources.util.get_fqdn_from_hosts" with mock.patch("cloudinit.sources.util.get_hostname") as m_gethost: with mock.patch(mock_fqdn) as m_fqdn: m_gethost.return_value = "systemhostname.domain.com" m_fqdn.return_value = None # No maching fqdn in /etc/hosts - self.assertEqual( - "systemhostname", datasource.get_hostname().hostname - ) - self.assertEqual( - "systemhostname.domain.com", - datasource.get_hostname(fqdn=True).hostname, + assert "systemhostname" == datasource.get_hostname().hostname + assert ( + "systemhostname.domain.com" + == datasource.get_hostname(fqdn=True).hostname ) - def test_get_hostname_without_metadata_returns_none(self): + def test_get_hostname_without_metadata_returns_none(self, datasource): """Datasource.gethostname returns None when metadata_only and no MD.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) - self.assertEqual({}, datasource.metadata) + assert {} == datasource.metadata mock_fqdn = "cloudinit.sources.util.get_fqdn_from_hosts" with mock.patch("cloudinit.sources.util.get_hostname") as m_gethost: with mock.patch(mock_fqdn) as m_fqdn: - self.assertIsNone( + assert ( datasource.get_hostname(metadata_only=True).hostname + is None ) - self.assertIsNone( + assert ( datasource.get_hostname( fqdn=True, metadata_only=True ).hostname + is None ) - self.assertEqual([], m_gethost.call_args_list) - self.assertEqual([], m_fqdn.call_args_list) + assert [] == m_gethost.call_args_list + assert [] == m_fqdn.call_args_list - def test_get_hostname_without_metadata_prefers_etc_hosts(self): + def test_get_hostname_without_metadata_prefers_etc_hosts(self, datasource): """Datasource.gethostname prefers /etc/hosts to util.get_hostname.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) - self.assertEqual({}, datasource.metadata) + assert {} == datasource.metadata mock_fqdn = "cloudinit.sources.util.get_fqdn_from_hosts" with mock.patch("cloudinit.sources.util.get_hostname") as m_gethost: with mock.patch(mock_fqdn) as m_fqdn: m_gethost.return_value = "systemhostname.domain.com" m_fqdn.return_value = "fqdnhostname.domain.com" - self.assertEqual( - "fqdnhostname", datasource.get_hostname().hostname - ) - self.assertEqual( - "fqdnhostname.domain.com", - datasource.get_hostname(fqdn=True).hostname, + assert "fqdnhostname" == datasource.get_hostname().hostname + assert ( + "fqdnhostname.domain.com" + == datasource.get_hostname(fqdn=True).hostname ) - def test_get_data_does_not_write_instance_data_on_failure(self): + def test_get_data_does_not_write_instance_data_on_failure(self, paths): """get_data does not write INSTANCE_JSON_FILE on get_data False.""" - tmp = self.tmp_dir() - paths = Paths({"run_dir": tmp}) datasource = DataSourceTestSubclassNet( self.sys_cfg, self.distro, paths, get_data_retval=False, ) - self.assertFalse(datasource.get_data()) + assert datasource.get_data() is False json_file = paths.get_runpath("instance_data") - self.assertFalse( - os.path.exists(json_file), f"Found unexpected file {json_file}" - ) + assert not os.path.exists( + json_file + ), f"Found unexpected file {json_file}" - def test_get_data_writes_json_instance_data_on_success(self): + def test_get_data_writes_json_instance_data_on_success( + self, datasource, paths + ): """get_data writes INSTANCE_JSON_FILE to run_dir as world readable.""" - tmp = self.tmp_dir() - datasource = DataSourceTestSubclassNet( - self.sys_cfg, self.distro, Paths({"run_dir": tmp}) - ) sys_info = { "python": "3.7", "platform": ( @@ -396,7 +363,7 @@ def test_get_data_writes_json_instance_data_on_success(self): return_value="canonical_cloud_id", ): datasource.get_data() - json_file = Paths({"run_dir": tmp}).get_runpath("instance_data") + json_file = paths.get_runpath("instance_data") content = util.load_text_file(json_file) expected = { "base64_encoded_keys": [], @@ -439,18 +406,17 @@ def test_get_data_writes_json_instance_data_on_success(self): }, }, } - self.assertEqual(expected, util.load_json(content)) + assert expected == util.load_json(content) file_stat = os.stat(json_file) - self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode)) - self.assertEqual(expected, util.load_json(content)) + assert 0o644 == stat.S_IMODE(file_stat.st_mode) + assert expected == util.load_json(content) - def test_get_data_writes_redacted_public_json_instance_data(self): + def test_get_data_writes_redacted_public_json_instance_data(self, paths): """get_data writes redacted content to public INSTANCE_JSON_FILE.""" - tmp = self.tmp_dir() datasource = DataSourceTestSubclassNet( self.sys_cfg, self.distro, - Paths({"run_dir": tmp}), + paths, custom_metadata={ "availability_zone": "myaz", "local-hostname": "test-subclass-hostname", @@ -469,7 +435,7 @@ def test_get_data_writes_redacted_public_json_instance_data(self): "VENDOR-DAta": "HIDE ME TOO", }, ) - self.assertCountEqual( + assert_count_equal( ( "combined_cloud_config", "merged_cfg", @@ -501,7 +467,7 @@ def test_get_data_writes_redacted_public_json_instance_data(self): } with mock.patch("cloudinit.util.system_info", return_value=sys_info): datasource.get_data() - json_file = Paths({"run_dir": tmp}).get_runpath("instance_data") + json_file = paths.get_runpath("instance_data") redacted = util.load_json(util.load_text_file(json_file)) expected = { "base64_encoded_keys": [], @@ -555,19 +521,18 @@ def test_get_data_writes_redacted_public_json_instance_data(self): }, }, } - self.assertEqual(expected, redacted) + assert expected == redacted file_stat = os.stat(json_file) - self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode)) + assert 0o644 == stat.S_IMODE(file_stat.st_mode) - def test_get_data_writes_json_instance_data_sensitive(self): + def test_get_data_writes_json_instance_data_sensitive(self, paths): """ get_data writes unmodified data to sensitive file as root-readonly. """ - tmp = self.tmp_dir() datasource = DataSourceTestSubclassNet( self.sys_cfg, self.distro, - Paths({"run_dir": tmp}), + paths, custom_metadata={ "availability_zone": "myaz", "local-hostname": "test-subclass-hostname", @@ -596,7 +561,7 @@ def test_get_data_writes_json_instance_data_sensitive(self): "dist": ["ubuntu", "20.04", "focal"], } - self.assertCountEqual( + assert_count_equal( ( "combined_cloud_config", "merged_cfg", @@ -617,9 +582,7 @@ def test_get_data_writes_json_instance_data_sensitive(self): return_value="canonical-cloud-id", ): datasource.get_data() - sensitive_json_file = Paths({"run_dir": tmp}).get_runpath( - "instance_data_sensitive" - ) + sensitive_json_file = paths.get_runpath("instance_data_sensitive") content = util.load_text_file(sensitive_json_file) expected = { "base64_encoded_keys": [], @@ -684,20 +647,18 @@ def test_get_data_writes_json_instance_data_sensitive(self): }, }, } - self.assertCountEqual(expected, util.load_json(content)) + assert_count_equal(expected, util.load_json(content)) file_stat = os.stat(sensitive_json_file) - self.assertEqual(0o600, stat.S_IMODE(file_stat.st_mode)) - self.assertEqual(expected, util.load_json(content)) + assert 0o600 == stat.S_IMODE(file_stat.st_mode) + assert expected == util.load_json(content) - def test_get_data_handles_redacted_unserializable_content(self): + def test_get_data_handles_redacted_unserializable_content(self, paths): """get_data warns unserializable content in INSTANCE_JSON_FILE.""" - tmp = self.tmp_dir() - paths = Paths({"run_dir": tmp}) datasource = DataSourceTestSubclassNet( self.sys_cfg, self.distro, paths, - custom_metadata={"key1": "val1", "key2": {"key2.1": self.paths}}, + custom_metadata={"key1": "val1", "key2": {"key2.1": paths}}, ) datasource.get_data() json_file = paths.get_runpath("instance_data") @@ -712,14 +673,13 @@ def test_get_data_handles_redacted_unserializable_content(self): }, } instance_json = util.load_json(content) - self.assertEqual(expected_metadata, instance_json["ds"]["meta_data"]) + assert expected_metadata == instance_json["ds"]["meta_data"] - def test_persist_instance_data_writes_ec2_metadata_when_set(self): + def test_persist_instance_data_writes_ec2_metadata_when_set( + self, datasource, paths + ): """When ec2_metadata class attribute is set, persist to json.""" - tmp = self.tmp_dir() - cloud_dir = os.path.join(tmp, "cloud") - util.ensure_dir(cloud_dir) - paths = Paths({"run_dir": tmp, "cloud_dir": cloud_dir}) + util.ensure_dir(paths.cloud_dir) datasource = DataSourceTestSubclassNet( self.sys_cfg, self.distro, @@ -729,17 +689,17 @@ def test_persist_instance_data_writes_ec2_metadata_when_set(self): datasource.get_data() json_file = paths.get_runpath("instance_data") instance_data = util.load_json(util.load_text_file(json_file)) - self.assertNotIn("ec2_metadata", instance_data["ds"]) + assert "ec2_metadata" not in instance_data["ds"] datasource.ec2_metadata = {"ec2stuff": "is good"} datasource.persist_instance_data() instance_data = util.load_json(util.load_text_file(json_file)) - self.assertEqual( - {"ec2stuff": "is good"}, instance_data["ds"]["ec2_metadata"] - ) + assert {"ec2stuff": "is good"} == instance_data["ds"]["ec2_metadata"] - def test_persist_instance_data_writes_canonical_cloud_id_and_symlink(self): + def test_persist_instance_data_writes_canonical_cloud_id_and_symlink( + self, tmp_path + ): """canonical-cloud-id class attribute is set, persist to json.""" - tmp = self.tmp_dir() + tmp = str(tmp_path) cloud_dir = os.path.join(tmp, "cloud") util.ensure_dir(cloud_dir) datasource = DataSourceTestSubclassNet( @@ -751,35 +711,39 @@ def test_persist_instance_data_writes_canonical_cloud_id_and_symlink(self): cloud_id_file = os.path.join(tmp, "cloud-id-my-cloud") cloud_id2_file = os.path.join(tmp, "cloud-id-my-cloud2") for filename in (cloud_id_file, cloud_id_link, cloud_id2_file): - self.assertFalse( - os.path.exists(filename), "Unexpected link found {filename}" - ) + assert not os.path.exists( + filename + ), f"Unexpected link found {filename}" with mock.patch( "cloudinit.sources.canonical_cloud_id", return_value="my-cloud" ): datasource.get_data() - self.assertEqual("my-cloud\n", util.load_text_file(cloud_id_link)) + assert "my-cloud\n" == util.load_text_file(cloud_id_link) # A symlink with the generic /run/cloud-init/cloud-id # link is present - self.assertTrue(util.is_link(cloud_id_link)) + assert util.is_link(cloud_id_link) datasource.persist_instance_data() # cloud-id not deleted: no cloud-id change - self.assertTrue(os.path.exists(cloud_id_file)) + assert os.path.exists(cloud_id_file) # When cloud-id changes, symlink and content change with mock.patch( "cloudinit.sources.canonical_cloud_id", return_value="my-cloud2" ): datasource.persist_instance_data() - self.assertEqual("my-cloud2\n", util.load_text_file(cloud_id2_file)) + assert "my-cloud2\n" == util.load_text_file(cloud_id2_file) # Previous cloud-id- file removed - self.assertFalse(os.path.exists(cloud_id_file)) + assert not os.path.exists( + cloud_id_file + ), f"Unexpected {filename} not removed" # Generic link persisted which contains canonical-cloud-id as content - self.assertTrue(util.is_link(cloud_id_link)) - self.assertEqual("my-cloud2\n", util.load_text_file(cloud_id_link)) + assert util.is_link(cloud_id_link) + assert "my-cloud2\n" == util.load_text_file(cloud_id_link) - def test_persist_instance_data_writes_network_json_when_set(self): + def test_persist_instance_data_writes_network_json_when_set( + self, tmp_path + ): """When network_data.json class attribute is set, persist to json.""" - tmp = self.tmp_dir() + tmp = str(tmp_path) cloud_dir = os.path.join(tmp, "cloud") util.ensure_dir(cloud_dir) paths = Paths({"run_dir": tmp, "cloud_dir": cloud_dir}) @@ -791,17 +755,17 @@ def test_persist_instance_data_writes_network_json_when_set(self): datasource.get_data() json_file = paths.get_runpath("instance_data") instance_data = util.load_json(util.load_text_file(json_file)) - self.assertNotIn("network_json", instance_data["ds"]) + assert "network_json" not in instance_data["ds"] datasource.network_json = {"network_json": "is good"} datasource.persist_instance_data() instance_data = util.load_json(util.load_text_file(json_file)) - self.assertEqual( - {"network_json": "is good"}, instance_data["ds"]["network_json"] - ) + assert {"network_json": "is good"} == instance_data["ds"][ + "network_json" + ] - def test_persist_instance_serializes_datasource_pickle(self): + def test_persist_instance_serializes_datasource_pickle(self, tmp_path): """obj.pkl is written when instance link present and write_cache.""" - tmp = self.tmp_dir() + tmp = str(tmp_path) cloud_dir = os.path.join(tmp, "cloud") util.ensure_dir(cloud_dir) datasource = DataSourceTestSubclassNet( @@ -810,26 +774,26 @@ def test_persist_instance_serializes_datasource_pickle(self): Paths({"run_dir": tmp, "cloud_dir": cloud_dir}), ) pkl_cache_file = os.path.join(cloud_dir, "instance/obj.pkl") - self.assertFalse(os.path.exists(pkl_cache_file)) + assert not os.path.exists(pkl_cache_file) datasource.network_json = {"network_json": "is good"} # No /var/lib/cloud/instance symlink datasource.persist_instance_data(write_cache=True) - self.assertFalse(os.path.exists(pkl_cache_file)) + assert not os.path.exists(pkl_cache_file) # Symlink /var/lib/cloud/instance but write_cache=False util.sym_link(cloud_dir, os.path.join(cloud_dir, "instance")) datasource.persist_instance_data(write_cache=False) - self.assertFalse(os.path.exists(pkl_cache_file)) + assert not os.path.exists(pkl_cache_file) # Symlink /var/lib/cloud/instance and write_cache=True datasource.persist_instance_data(write_cache=True) - self.assertTrue(os.path.exists(pkl_cache_file)) + assert os.path.exists(pkl_cache_file) ds = pkl_load(pkl_cache_file) - self.assertEqual(datasource.network_json, ds.network_json) + assert datasource.network_json == ds.network_json - def test_get_data_base64encodes_unserializable_bytes(self): + def test_get_data_base64encodes_unserializable_bytes(self, tmp_path): """On py3, get_data base64encodes any unserializable content.""" - tmp = self.tmp_dir() + tmp = str(tmp_path) paths = Paths({"run_dir": tmp}) datasource = DataSourceTestSubclassNet( self.sys_cfg, @@ -837,17 +801,16 @@ def test_get_data_base64encodes_unserializable_bytes(self): paths, custom_metadata={"key1": "val1", "key2": {"key2.1": b"\x123"}}, ) - self.assertTrue(datasource.get_data()) + assert datasource.get_data() is True json_file = paths.get_runpath("instance_data") content = util.load_text_file(json_file) instance_json = util.load_json(content) - self.assertCountEqual( + assert_count_equal( ["ds/meta_data/key2/key2.1"], instance_json["base64_encoded_keys"] ) - self.assertEqual( - {"key1": "val1", "key2": {"key2.1": "EjM="}}, - instance_json["ds"]["meta_data"], - ) + assert {"key1": "val1", "key2": {"key2.1": "EjM="}} == instance_json[ + "ds" + ]["meta_data"] def test_get_hostname_subclass_support(self): """Validate get_hostname signature on all subclasses of DataSource.""" @@ -863,17 +826,15 @@ def test_get_hostname_subclass_support(self): for child in DataSource.__subclasses__(): if "Test" in child.dsname: continue - self.assertEqual( - base_args, - inspect.getfullargspec(child.get_hostname), - "%s does not implement DataSource.get_hostname params" % child, + assert base_args == inspect.getfullargspec(child.get_hostname), ( + "%s does not implement DataSource.get_hostname params" % child ) for grandchild in child.__subclasses__(): - self.assertEqual( - base_args, - inspect.getfullargspec(grandchild.get_hostname), + assert base_args == inspect.getfullargspec( + grandchild.get_hostname + ), ( "%s does not implement DataSource.get_hostname params" - % grandchild, + % grandchild ) def test_clear_cached_attrs_resets_cached_attr_class_attributes(self): @@ -886,7 +847,7 @@ def test_clear_cached_attrs_resets_cached_attr_class_attributes(self): self.datasource._dirty_cache = True self.datasource.clear_cached_attrs() for attr, value in self.datasource.cached_attr_defaults: - self.assertEqual(value, getattr(self.datasource, attr)) + assert value == getattr(self.datasource, attr) def test_clear_cached_attrs_noops_on_clean_cache(self): """Class attributes listed in cached_attr_defaults are reset.""" @@ -899,14 +860,14 @@ def test_clear_cached_attrs_noops_on_clean_cache(self): self.datasource.clear_cached_attrs() count = 0 for attr, _ in self.datasource.cached_attr_defaults: - self.assertEqual(count, getattr(self.datasource, attr)) + assert count == getattr(self.datasource, attr) count += 1 def test_clear_cached_attrs_skips_non_attr_class_attributes(self): """Skip any cached_attr_defaults which aren't class attributes.""" self.datasource._dirty_cache = True self.datasource.clear_cached_attrs(attr_defaults=(("some", "value"),)) - self.assertFalse(hasattr(self.datasource, "some")) + assert not hasattr(self.datasource, "some") def test_clear_cached_attrs_of_custom_attrs(self): """Custom attr_values can be passed to clear_cached_attrs.""" @@ -917,8 +878,8 @@ def test_clear_cached_attrs_of_custom_attrs(self): self.datasource.clear_cached_attrs( attr_defaults=(("myattr", "updated"),) ) - self.assertEqual("himom", getattr(self.datasource, cached_attr_name)) - self.assertEqual("updated", self.datasource.myattr) + assert "himom" == getattr(self.datasource, cached_attr_name) + assert "updated" == self.datasource.myattr @mock.patch.dict( DataSource.default_update_events, @@ -930,25 +891,24 @@ def test_clear_cached_attrs_of_custom_attrs(self): ) def test_update_metadata_only_acts_on_supported_update_events(self): """update_metadata_if_supported wont get_data on unsupported events.""" - self.assertEqual( - {EventScope.NETWORK: set([EventType.BOOT_NEW_INSTANCE])}, - self.datasource.default_update_events, - ) + assert { + EventScope.NETWORK: set([EventType.BOOT_NEW_INSTANCE]) + } == self.datasource.default_update_events fake_get_data = mock.Mock() self.datasource.get_data = fake_get_data - self.assertFalse( - self.datasource.update_metadata_if_supported( - source_event_types=[EventType.BOOT] - ) + assert not self.datasource.update_metadata_if_supported( + source_event_types=[EventType.BOOT] ) - self.assertEqual([], fake_get_data.call_args_list) + assert [] == fake_get_data.call_args_list @mock.patch.dict( DataSource.supported_update_events, {EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}}, ) - def test_update_metadata_returns_true_on_supported_update_event(self): + def test_update_metadata_returns_true_on_supported_update_event( + self, caplog + ): """update_metadata_if_supported returns get_data on supported events""" def fake_get_data(): @@ -957,34 +917,29 @@ def fake_get_data(): self.datasource.get_data = fake_get_data self.datasource._network_config = "something" self.datasource._dirty_cache = True - self.assertTrue( - self.datasource.update_metadata_if_supported( - source_event_types=[ - EventType.BOOT, - EventType.BOOT_NEW_INSTANCE, - ] - ) - ) - self.assertEqual(UNSET, self.datasource._network_config) - - self.assertIn( - "DEBUG: Update datasource metadata and network config due to" + assert self.datasource.update_metadata_if_supported( + source_event_types=[ + EventType.BOOT, + EventType.BOOT_NEW_INSTANCE, + ] + ) + assert UNSET == self.datasource._network_config + + assert ( + mock.ANY, + logging.DEBUG, + "Update datasource metadata and network config due to" " events: boot-new-instance", - self.logs.getvalue(), - ) + ) in caplog.record_tuples class TestRedactSensitiveData(CiTestCase): def test_redact_sensitive_data_noop_when_no_sensitive_keys_present(self): """When sensitive_keys is absent or empty from metadata do nothing.""" md = {"my": "data"} - self.assertEqual( - md, redact_sensitive_keys(md, redact_value="redacted") - ) + assert md == redact_sensitive_keys(md, redact_value="redacted") md["sensitive_keys"] = [] - self.assertEqual( - md, redact_sensitive_keys(md, redact_value="redacted") - ) + assert md == redact_sensitive_keys(md, redact_value="redacted") def test_redact_sensitive_data_redacts_exact_match_name(self): """Only exact matched sensitive_keys are redacted from metadata.""" @@ -994,9 +949,7 @@ def test_redact_sensitive_data_redacts_exact_match_name(self): } secure_md = copy.deepcopy(md) secure_md["md"]["secure"] = "redacted" - self.assertEqual( - secure_md, redact_sensitive_keys(md, redact_value="redacted") - ) + assert secure_md == redact_sensitive_keys(md, redact_value="redacted") def test_redact_sensitive_data_does_redacts_with_default_string(self): """When redact_value is absent, REDACT_SENSITIVE_VALUE is used.""" @@ -1006,89 +959,59 @@ def test_redact_sensitive_data_does_redacts_with_default_string(self): } secure_md = copy.deepcopy(md) secure_md["md"]["secure"] = "redacted for non-root user" - self.assertEqual(secure_md, redact_sensitive_keys(md)) + assert secure_md == redact_sensitive_keys(md) class TestCanonicalCloudID(CiTestCase): def test_cloud_id_returns_platform_on_unknowns(self): """When region and cloud_name are unknown, return platform.""" - self.assertEqual( - "platform", - canonical_cloud_id( - cloud_name=METADATA_UNKNOWN, - region=METADATA_UNKNOWN, - platform="platform", - ), + assert "platform" == canonical_cloud_id( + cloud_name=METADATA_UNKNOWN, + region=METADATA_UNKNOWN, + platform="platform", ) def test_cloud_id_returns_platform_on_none(self): """When region and cloud_name are unknown, return platform.""" - self.assertEqual( - "platform", - canonical_cloud_id( - cloud_name=None, region=None, platform="platform" - ), + assert "platform" == canonical_cloud_id( + cloud_name=None, region=None, platform="platform" ) def test_cloud_id_returns_cloud_name_on_unknown_region(self): """When region is unknown, return cloud_name.""" for region in (None, METADATA_UNKNOWN): - self.assertEqual( - "cloudname", - canonical_cloud_id( - cloud_name="cloudname", region=region, platform="platform" - ), + assert "cloudname" == canonical_cloud_id( + cloud_name="cloudname", region=region, platform="platform" ) def test_cloud_id_returns_platform_on_unknown_cloud_name(self): """When region is set but cloud_name is unknown return cloud_name.""" - self.assertEqual( - "platform", - canonical_cloud_id( - cloud_name=METADATA_UNKNOWN, - region="region", - platform="platform", - ), + assert "platform" == canonical_cloud_id( + cloud_name=METADATA_UNKNOWN, + region="region", + platform="platform", ) def test_cloud_id_aws_based_on_region_and_cloud_name(self): """When cloud_name is aws, return proper cloud-id based on region.""" - self.assertEqual( - "aws-china", - canonical_cloud_id( - cloud_name="aws", region="cn-north-1", platform="platform" - ), + assert "aws-china" == canonical_cloud_id( + cloud_name="aws", region="cn-north-1", platform="platform" ) - self.assertEqual( - "aws", - canonical_cloud_id( - cloud_name="aws", region="us-east-1", platform="platform" - ), + assert "aws" == canonical_cloud_id( + cloud_name="aws", region="us-east-1", platform="platform" ) - self.assertEqual( - "aws-gov", - canonical_cloud_id( - cloud_name="aws", region="us-gov-1", platform="platform" - ), + assert "aws-gov" == canonical_cloud_id( + cloud_name="aws", region="us-gov-1", platform="platform" ) - self.assertEqual( # Overrideen non-aws cloud_name is returned - "!aws", - canonical_cloud_id( - cloud_name="!aws", region="us-gov-1", platform="platform" - ), + assert "!aws" == canonical_cloud_id( + cloud_name="!aws", region="us-gov-1", platform="platform" ) def test_cloud_id_azure_based_on_region_and_cloud_name(self): """Report cloud-id when cloud_name is azure and region is in china.""" - self.assertEqual( - "azure-china", - canonical_cloud_id( - cloud_name="azure", region="chinaeast", platform="platform" - ), + assert "azure-china" == canonical_cloud_id( + cloud_name="azure", region="chinaeast", platform="platform" ) - self.assertEqual( - "azure", - canonical_cloud_id( - cloud_name="azure", region="!chinaeast", platform="platform" - ), + assert "azure" == canonical_cloud_id( + cloud_name="azure", region="!chinaeast", platform="platform" ) From 10d933bd86498619189b56b355b9157c1058a5e5 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 7 Aug 2025 15:29:37 +0200 Subject: [PATCH 059/150] fix: Correct v2 ENI route and dns rendering (#6331) Network v2 route definitions can have mixed ipv4 and ipv6 routes, render (in ENI) each type into its appropiated ipvX subnet section. Creating an ipv6 if no other is present. Similarly, render ipvX dns-search to ipvX subnet sections and render dns-nameserver to all of them (more correct on older dns implementations that do not take into account global dns settings). Fixes: ENI part of GH-4518 Signed-off-by: Alberto Contreras --- cloudinit/net/eni.py | 89 ++++++++- tests/unittests/net/network_configs.py | 244 ++++++++++++++++++++++++- tests/unittests/test_net.py | 11 +- 3 files changed, 330 insertions(+), 14 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index b363acbb392..a580ff0de64 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -1,6 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import copy +import functools import glob import logging import os @@ -9,7 +10,15 @@ from typing import Any, Dict, List, Optional from cloudinit import performance, subp, util -from cloudinit.net import ParserError, renderer, subnet_is_ipv6 +from cloudinit.net import ( + ParserError, + is_ipv4_address, + is_ipv4_network, + is_ipv6_address, + is_ipv6_network, + renderer, + subnet_is_ipv6, +) from cloudinit.net.network_state import NetworkState LOG = logging.getLogger(__name__) @@ -62,7 +71,7 @@ # TODO: switch valid_map based on mode inet/inet6 -def _iface_add_subnet(iface: dict, subnet: dict) -> List[str]: +def _iface_add_subnet(iface: dict, subnet: dict, is_ipv6: bool) -> List[str]: content = [] valid_map = [ "address", @@ -83,7 +92,21 @@ def _iface_add_subnet(iface: dict, subnet: dict) -> List[str]: value = "%s/%s" % (subnet["address"], subnet["prefix"]) if value and key in valid_map: if isinstance(value, list): + if key == "dns_nameservers": + value = list( + filter( + functools.partial( + has_same_ip_version, is_ipv6=is_ipv6 + ), + value, + ) + ) value = " ".join(value) + else: + if key == "dns_nameservers" and not has_same_ip_version( + value, is_ipv6 + ): + continue if "_" in key: key = key.replace("_", "-") content.append(" {0} {1}".format(key, value)) @@ -367,6 +390,12 @@ def _ifaces_to_net_config_data(ifaces: dict) -> dict: return {"version": 1, "config": [devs[d] for d in sorted(devs)]} +def has_same_ip_version(addr_or_net: str, is_ipv6: bool) -> bool: + if not is_ipv6: + return is_ipv4_address(addr_or_net) or is_ipv4_network(addr_or_net) + return is_ipv6_address(addr_or_net) or is_ipv6_network(addr_or_net) + + class Renderer(renderer.Renderer): """Renders network information in a /etc/network/interfaces format.""" @@ -417,7 +446,7 @@ def _render_route(self, route: dict, indent: str = "") -> List[str]: route_line += "%s %s %s" % (default_gw, mapping[k], route[k]) elif k in route: if k == "network": - if ":" in route[k]: + if is_ipv6_address(route[k]): route_line += " -A inet6" elif route.get("prefix") == 32: route_line += " -host" @@ -447,6 +476,8 @@ def _render_iface( # Specify WOL setting 'g' for using "Magic Packet" iface["ethernet-wol"] = "g" if subnets: + dns = None + routes6 = [] for index, subnet in enumerate(subnets): ipv4_subnet_mtu = None iface["index"] = index @@ -454,8 +485,10 @@ def _render_iface( iface["control"] = subnet.get("control", "auto") subnet_inet = "inet" if subnet_is_ipv6(subnet): + is_ipv6 = True subnet_inet += "6" else: + is_ipv6 = False ipv4_subnet_mtu = subnet.get("mtu") iface["inet"] = subnet_inet if ( @@ -492,16 +525,60 @@ def _render_iface( ]: iface["control"] = "alias" + # v1 config has the dns info in the first non-dhcp route, + # replicate dns to others to be more correct + dns_present = ( + "dns_search" in subnet or "dns_nameservers" in subnet + ) + if dns is None and dns_present: + dns = dict( + (k, subnet.get(k)) + for k in ("dns_search", "dns_nameservers") + ) + if dns is not None and not dns_present: + subnet = {**subnet, **dns} + lines = list( _iface_start_entry( iface, index, render_hwaddress=render_hwaddress ) - + _iface_add_subnet(iface, subnet) + + _iface_add_subnet(iface, subnet, is_ipv6) + _iface_add_attrs(iface, index, ipv4_subnet_mtu) ) for route in subnet.get("routes", []): + ipv6_network = is_ipv6_network(route.get("network", "")) + if ipv6_network and not is_ipv6: + routes6.append(route) + continue lines.extend(self._render_route(route, indent=" ")) + if routes6 and is_ipv6: + for route in routes6: + lines.extend(self._render_route(route, indent=" ")) + routes6.clear() + + sections.append(lines) + + if routes6: + # no ipv6 subnet found create a static one to add remaining + # routes: + iface = { + "name": iface["name"], + "control": iface["control"], + "mode": "static", + "inet": "inet6", + } + subnet = {"type": "static", "routes": routes6} + if dns is not None: + subnet = {**subnet, **dns} + lines = list( + _iface_start_entry( + iface, -1, render_hwaddress=render_hwaddress + ) + + _iface_add_subnet(iface, subnet, True) + ) + for route in subnet["routes"]: + lines.extend(self._render_route(route, indent=" ")) sections.append(lines) else: # ifenslave docs say to auto the slave devices @@ -534,11 +611,11 @@ def _render_interfaces( nameservers = network_state.dns_nameservers if nameservers: - lo["subnets"][0]["dns_nameservers"] = " ".join(nameservers) + lo["subnets"][0]["dns_nameservers"] = nameservers searchdomains = network_state.dns_searchdomains if searchdomains: - lo["subnets"][0]["dns_search"] = " ".join(searchdomains) + lo["subnets"][0]["dns_search"] = searchdomains # Apply a sort order to ensure that we write out the physical # interfaces first; this is critical for bonding diff --git a/tests/unittests/net/network_configs.py b/tests/unittests/net/network_configs.py index fab4e3197fc..c2fd524a393 100644 --- a/tests/unittests/net/network_configs.py +++ b/tests/unittests/net/network_configs.py @@ -1585,6 +1585,8 @@ # control-alias eth0.101 iface eth0.101 inet static address 192.168.2.10/24 + dns-nameservers 192.168.0.10 10.23.23.134 + dns-search barley.maas sacchromyces.maas brettanomyces.maas post-up route add -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true @@ -4602,6 +4604,39 @@ ), }, "v2-mixed-routes": { + "expected_eni": textwrap.dedent( + """\ + auto lo + iface lo inet loopback + + auto eth0 + iface eth0 inet dhcp + mtu 500 + post-up route add -host 169.254.42.42/32 gw 62.210.0.1 || true + pre-down route del -host 169.254.42.42/32 gw 62.210.0.1 || true + post-up route add -host 169.254.42.43/32 gw 62.210.0.2 || true + pre-down route del -host 169.254.42.43/32 gw 62.210.0.2 || true + + # control-alias eth0 + iface eth0 inet6 dhcp + post-up route add -A inet6 default gw fe80::dc00:ff:fe20:186 || true + pre-down route del -A inet6 default gw fe80::dc00:ff:fe20:186 || true + post-up route add -A inet6 fe80::dc00:ff:fe20:188/64 gw fe80::dc00:ff:fe20:187 || true + pre-down route del -A inet6 fe80::dc00:ff:fe20:188/64 gw fe80::dc00:ff:fe20:187 || true + + # control-alias eth0 + iface eth0 inet static + address 192.168.1.20/16 + dns-nameservers 8.8.8.8 + dns-search lab home + + # control-alias eth0 + iface eth0 inet6 static + address 2001:bc8:1210:232:dc00:ff:fe20:185/64 + dns-nameservers FEDC::1 + dns-search lab home + """ # noqa: E501 + ), "expected_network_manager": { "cloud-init-eth0.nmconnection": textwrap.dedent( """\ @@ -4675,6 +4710,190 @@ """ ), }, + "v2-mixed-routes-reversed": { + "expected_eni": textwrap.dedent( + """\ + auto lo + iface lo inet loopback + + auto eth0 + iface eth0 inet dhcp + mtu 500 + post-up route add -host 169.254.42.42/32 gw 62.210.0.1 || true + pre-down route del -host 169.254.42.42/32 gw 62.210.0.1 || true + post-up route add -host 169.254.42.43/32 gw 62.210.0.2 || true + pre-down route del -host 169.254.42.43/32 gw 62.210.0.2 || true + + # control-alias eth0 + iface eth0 inet6 dhcp + post-up route add -A inet6 default gw fe80::dc00:ff:fe20:186 || true + pre-down route del -A inet6 default gw fe80::dc00:ff:fe20:186 || true + post-up route add -A inet6 fe80::dc00:ff:fe20:188/64 gw fe80::dc00:ff:fe20:187 || true + pre-down route del -A inet6 fe80::dc00:ff:fe20:188/64 gw fe80::dc00:ff:fe20:187 || true + + # control-alias eth0 + iface eth0 inet6 static + address 2001:bc8:1210:232:dc00:ff:fe20:185/64 + dns-nameservers FEDC::1 + dns-search home lab + + # control-alias eth0 + iface eth0 inet static + address 192.168.1.20/16 + dns-nameservers 8.8.8.8 + dns-search home lab + """ # noqa: E501 + ), + "expected_network_manager": { + "cloud-init-eth0.nmconnection": textwrap.dedent( + """\ + # Generated by cloud-init. Changes will be lost. + + [connection] + id=cloud-init eth0 + uuid=1dd9a779-d327-56e1-8454-c65e2556c12c + autoconnect-priority=120 + type=ethernet + interface-name=eth0 + + [user] + org.freedesktop.NetworkManager.origin=cloud-init + + [ethernet] + mtu=500 + + [ipv4] + method=auto + may-fail=true + route1=169.254.42.42/32,62.210.0.1 + route1_options=mtu=400 + route2=169.254.42.43/32,62.210.0.2 + route2_options=mtu=200 + address1=192.168.1.20/16 + dns=8.8.8.8; + dns-search=home;lab; + + [ipv6] + route1=::/0,fe80::dc00:ff:fe20:186 + route1_options=mtu=300 + route2=fe80::dc00:ff:fe20:188/64,fe80::dc00:ff:fe20:187 + route2_options=mtu=100 + method=auto + may-fail=true + address1=2001:bc8:1210:232:dc00:ff:fe20:185/64 + dns=FEDC::1; + dns-search=home;lab; + + """ + ) + }, + "yaml": textwrap.dedent( + """\ + version: 2 + ethernets: + eth0: + dhcp6: true + dhcp4: true + mtu: 500 + nameservers: + search: [home, lab] + addresses: ["FEDC::1", 8.8.8.8] + routes: + - via: fe80::dc00:ff:fe20:186 + to: ::/0 + mtu: 300 + - to: 169.254.42.42/32 + via: 62.210.0.1 + mtu: 400 + - via: fe80::dc00:ff:fe20:187 + to: fe80::dc00:ff:fe20:188 + mtu: 100 + - to: 169.254.42.43/32 + via: 62.210.0.2 + mtu: 200 + addresses: + - 2001:bc8:1210:232:dc00:ff:fe20:185/64 + - 192.168.1.20/16 + """ + ), + }, + "v2-mixed-routes-no-ipv6-addr": { + "expected_eni": textwrap.dedent( + """\ + auto lo + iface lo inet loopback + + auto eth0 + iface eth0 inet dhcp + post-up route add -host 169.254.42.42/32 gw 62.210.0.1 || true + pre-down route del -host 169.254.42.42/32 gw 62.210.0.1 || true + + # control-alias eth0 + iface eth0 inet static + address 192.168.1.20/16 + dns-nameservers 8.8.8.8 + dns-search lab home + + # control-alias eth0 + iface eth0 inet6 static + dns-nameservers FEDC::1 + dns-search lab home + post-up route add -A inet6 default gw fe80::dc00:ff:fe20:186 || true + pre-down route del -A inet6 default gw fe80::dc00:ff:fe20:186 || true + """ # noqa: E501 + ), + "expected_network_manager": { + "cloud-init-eth0.nmconnection": textwrap.dedent( + """\ + # Generated by cloud-init. Changes will be lost. + + [connection] + id=cloud-init eth0 + uuid=1dd9a779-d327-56e1-8454-c65e2556c12c + autoconnect-priority=120 + type=ethernet + interface-name=eth0 + + [user] + org.freedesktop.NetworkManager.origin=cloud-init + + [ethernet] + + [ipv4] + method=auto + may-fail=false + route1=169.254.42.42/32,62.210.0.1 + address1=192.168.1.20/16 + dns=8.8.8.8; + dns-search=lab;home; + + [ipv6] + route1=::/0,fe80::dc00:ff:fe20:186 + dns=FEDC::1; + dns-search=lab;home; + + """ + ) + }, + "yaml": textwrap.dedent( + """\ + version: 2 + ethernets: + eth0: + dhcp4: true + nameservers: + search: [lab, home] + addresses: [8.8.8.8, "FEDC::1"] + routes: + - to: 169.254.42.42/32 + via: 62.210.0.1 + - via: fe80::dc00:ff:fe20:186 + to: ::/0 + addresses: + - 192.168.1.20/16 + """ + ), + }, "v2-dns": { "expected_networkd": textwrap.dedent( """\ @@ -4695,11 +4914,6 @@ ), "expected_eni": textwrap.dedent( """\ - # This file is generated from information provided by the datasource. Changes - # to it will not persist across an instance reboot. To disable cloud-init's - # network configuration capabilities, write a file - # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: - # network: {config: disabled} auto lo iface lo inet loopback @@ -4800,6 +5014,18 @@ ), }, "v2-dns-no-if-ips": { + "expected_eni": textwrap.dedent( + """\ + auto lo + iface lo inet loopback + + auto eth0 + iface eth0 inet dhcp + + # control-alias eth0 + iface eth0 inet6 dhcp + """ # noqa: E501 + ), "expected_network_manager": { "cloud-init-eth0.nmconnection": textwrap.dedent( """\ @@ -4846,6 +5072,14 @@ ), }, "v2-dns-no-dhcp": { + "expected_eni": textwrap.dedent( + """\ + auto lo + iface lo inet loopback + + iface eth0 inet manual + """ # noqa: E501 + ), "expected_network_manager": { "cloud-init-eth0.nmconnection": textwrap.dedent( """\ diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 8f9468796d3..b60dedf44b2 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -3215,6 +3215,8 @@ def test_ipv4_and_ipv6_static_config(self, yaml_file, config, caplog): ("v6_and_v4", "yaml"), ("v1-dns", "yaml"), ("v2-mixed-routes", "yaml"), + ("v2-mixed-routes-reversed", "yaml"), + ("v2-mixed-routes-no-ipv6-addr", "yaml"), ("v2-dns", "yaml"), ("v2-dns-no-if-ips", "yaml"), ("v2-dns-no-dhcp", "yaml"), @@ -4540,9 +4542,12 @@ def testsimple_convert_and_render(self): pytest.param( "v1-dns", "yaml", marks=pytest.mark.xfail(reason="GH-4219") ), - pytest.param( - "v2-dns", "yaml", marks=pytest.mark.xfail(reason="GH-4219") - ), + ("v2-dns", "yaml"), + ("v2-mixed-routes", "yaml"), + ("v2-mixed-routes-reversed", "yaml"), + ("v2-mixed-routes-no-ipv6-addr", "yaml"), + ("v2-dns-no-if-ips", "yaml"), + ("v2-dns-no-dhcp", "yaml"), ], ) def test_config(self, expected_name, yaml_version): From c1551ade3410a833cf88102a7313dd25fb77c676 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 8 Aug 2025 00:19:47 -0500 Subject: [PATCH 060/150] test: relax checks on authorized keys (#6361) Check that the expected keys exist rather than enforcing a specific number of keys. Additional authorized keys that have not been specified in user data may be added to the default and root accounts. This includes keys specified at launch and may vary in number. --- tests/integration_tests/modules/test_ssh_keysfile.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py index bf7ce0e6f2c..fb88e79b8a3 100644 --- a/tests/integration_tests/modules/test_ssh_keysfile.py +++ b/tests/integration_tests/modules/test_ssh_keysfile.py @@ -40,15 +40,8 @@ def common_verify(client, expected_keys): # Ensure key is in the key file contents = client.read_from_file(filename) if user in ["ubuntu", "root"]: - lines = contents.split("\n") - if user == "root": - # Our personal public key gets added by pycloudlib in - # addition to the default `ssh_authorized_keys` - assert len(lines) == 2 - else: - # Clouds will insert the keys we've added to our accounts - # or for our launches - assert len(lines) >= 2 + # Our personal public key(s) from pycloudlib or otherwise provided + # at launch may be added to the default and/or root user assert keys.public_key.strip() in contents else: assert contents.strip() == keys.public_key.strip() From 58c2b9d47d742728dbff7001b672c957add3c0f6 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 8 Aug 2025 00:21:19 -0500 Subject: [PATCH 061/150] test: increase timeout on test_clean_package_install (#6362) Creating a snapshot and launching from that can take a long time. --- tests/integration_tests/test_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/test_upgrade.py b/tests/integration_tests/test_upgrade.py index 11d3d2224ba..b76dcfed026 100644 --- a/tests/integration_tests/test_upgrade.py +++ b/tests/integration_tests/test_upgrade.py @@ -201,7 +201,7 @@ def test_subsequent_boot_of_upgraded_package(session_cloud: IntegrationCloud): verify_clean_boot(instance) -@pytest.mark.timeout(300) # A failure here can leave us hanging +@pytest.mark.timeout(600) # A failure here can leave us hanging def test_clean_package_install(session_cloud: IntegrationCloud): """Test that the package install works after purge of old package.""" source = get_validated_source(session_cloud) From c7007900d6c5c6fbc8343fa827d042f32bc63ee6 Mon Sep 17 00:00:00 2001 From: xqs <41766985+qingshanxiao292311@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:18:10 +0800 Subject: [PATCH 062/150] fix: add openeuler hosts template to avoid cloud-init service failures (#6328) Fixes: GH-6327 Signed-off-by: qingshanxiao292311 <2357342134@qq.com> --- templates/hosts.openeuler.tmpl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 templates/hosts.openeuler.tmpl diff --git a/templates/hosts.openeuler.tmpl b/templates/hosts.openeuler.tmpl new file mode 100644 index 00000000000..ed41ad3da36 --- /dev/null +++ b/templates/hosts.openeuler.tmpl @@ -0,0 +1,24 @@ +## template:jinja +{# +This file /etc/cloud/templates/hosts.openeuler.tmpl is only utilized +if enabled in cloud-config. Specifically, in order to enable it +you need to add the following to config: + manage_etc_hosts: True +-#} +# Your system has configured 'manage_etc_hosts' as True. +# As a result, if you wish for changes to this file to persist +# then you will need to either +# a.) make changes to the master file in /etc/cloud/templates/hosts.openeuler.tmpl +# b.) change or remove the value of 'manage_etc_hosts' in +# /etc/cloud/cloud.cfg or cloud-config from user-data +# +# The following lines are desirable for IPv4 capable hosts +127.0.0.1 {{fqdn}} {{hostname}} +127.0.0.1 localhost.localdomain localhost +127.0.0.1 localhost4.localdomain4 localhost4 + +# The following lines are desirable for IPv6 capable hosts +::1 {{fqdn}} {{hostname}} +::1 localhost.localdomain localhost +::1 localhost6.localdomain6 localhost6 + From aa4a1d354e2dca3d39f73d422f9a975ce9a7cd26 Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Fri, 8 Aug 2025 13:39:12 -0600 Subject: [PATCH 063/150] test: handle TMPDIR != "/tmp" (#6356) Fix an issue in an environment where TMPDIR is not set to /tmp where 22 tests fail. These tests have codepaths that use TemporaryDirectory(). Create a TMPDIR equivalent under the fake_filesystem. --- tests/unittests/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index dbae8277e07..af0ed8913ef 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -119,9 +119,11 @@ def fake_filesystem(mocker, tmpdir, fake_filesystem_hook): """ # This allows fake_filesystem to be used with production code that # creates temporary directories. Functions like TemporaryDirectory() - # attempt to create a directory under "/tmp" assuming that it already - # exists, but then it fails because of the retargeting that happens here. - tmpdir.mkdir("tmp") + # attempt to create a directory under $TMPDIR (among other locations) + # assuming that it already exists, but then it fails because of the + # retargeting that happens here. + TMPDIR = os.getenv("TMPDIR", "/tmp") + Path(tmpdir, TMPDIR[1:]).mkdir(exist_ok=True) for mod, funcs in FS_FUNCS.items(): for f, nargs in funcs: From 1b908e331a7baee4b1f5020800caafa6e6250707 Mon Sep 17 00:00:00 2001 From: Ani Sinha Date: Mon, 11 Aug 2025 16:20:26 +0530 Subject: [PATCH 064/150] feat(clean): Add a new clean option to clean fstab entries (#6348) A new `cloud-init clean -c fstab` option is introduced that would clean all fstab entries that have "comment=cloudconfig" comment. It refactors code already present in DataSourceAzure.clean() method added as a part of the change 08a9dcf34445be572 ("feat(azure): Implement the clean callback for DataSourceAzure (#6321)") Fixes: GH-6341 Signed-off-by: Ani Sinha Co-authored-by: Alberto Contreras --- cloudinit/cmd/clean.py | 6 ++- cloudinit/config/cc_mounts.py | 26 +++++++++++ cloudinit/sources/DataSourceAzure.py | 26 +---------- doc/rtd/reference/cli.rst | 14 ++++-- tests/unittests/cmd/test_clean.py | 70 ++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 31 deletions(-) diff --git a/cloudinit/cmd/clean.py b/cloudinit/cmd/clean.py index 94a9bfb8f9d..1ad329053ae 100755 --- a/cloudinit/cmd/clean.py +++ b/cloudinit/cmd/clean.py @@ -13,6 +13,7 @@ import sys from cloudinit import settings, sources +from cloudinit.config import cc_mounts from cloudinit.distros import uses_systemd from cloudinit.log import log_util from cloudinit.net.netplan import CLOUDINIT_NETPLAN_FILE @@ -99,6 +100,7 @@ def get_parser(parser=None): "ssh_config", "network", "datasource", + "fstab", ], default=[], nargs="+", @@ -118,7 +120,7 @@ def remove_artifacts(init, remove_logs, remove_seed=False, remove_config=None): @param: remove_seed: Boolean. Set True to also delete seed subdir in paths.cloud_dir. @param: remove_config: List of strings. - Can be any of: all, network, ssh_config, datasource. + Can be any of: all, network, ssh_config, datasource, fstab. @returns: 0 on success, 1 otherwise. """ init.read_cfg() @@ -134,6 +136,8 @@ def remove_artifacts(init, remove_logs, remove_seed=False, remove_config=None): ): for conf in GEN_SSH_CONFIG_FILES: del_file(conf) + if remove_config and set(remove_config).intersection(["all", "fstab"]): + cc_mounts.cleanup_fstab() clean_datasource = remove_config and set(remove_config).intersection( ["all", "datasource"] diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py index a4bc44abdad..c3998e578be 100644 --- a/cloudinit/config/cc_mounts.py +++ b/cloudinit/config/cc_mounts.py @@ -514,6 +514,32 @@ def mount_if_needed( subp.subp(["systemctl", "daemon-reload"]) +def cleanup_fstab(ds_remove_entries: list = []) -> None: + if not os.path.exists(FSTAB_PATH): + return + + base_entry = [MNT_COMMENT] + + with open(FSTAB_PATH, "r") as f: + lines = f.readlines() + new_lines = [] + changed = False + for line in lines: + if all(entry in line for entry in [*base_entry, *ds_remove_entries]): + changed = True + continue + new_lines.append(line) + + # rewrite fstab + try: + if changed: + with open(FSTAB_PATH, "w") as f: + f.writelines(new_lines) + LOG.info("Removed resource disk entries from %s", FSTAB_PATH) + except Exception as e: + LOG.warning("Failed to clean resource disk entries from fstab: %s", e) + + def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: """Handle the mounts configuration.""" # fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index d340a0996c6..80ac60b77b0 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -1676,31 +1676,7 @@ def _cleanup_resourcedisk_fstab(self): by cloud-init i.e. lines containing "/dev/disk/cloud/azure_resource" and cloudconfig comment. """ - fstab_path = cc_mounts.FSTAB_PATH - mnt_comment = cc_mounts.MNT_COMMENT - removed_entries = RESOURCE_DISK_PATH - if not os.path.exists(fstab_path): - return - - with open(fstab_path, "r") as f: - lines = f.readlines() - new_lines = [] - changed = False - for line in lines: - if removed_entries in line and mnt_comment in line: - changed = True - continue - new_lines.append(line) - # rewrite fstab - try: - if changed: - with open(fstab_path, "w") as f: - f.writelines(new_lines) - LOG.info("Removed resource disk entries from %s", fstab_path) - except Exception as e: - LOG.warning( - "Failed to clean resource disk entries from fstab: %s", e - ) + cc_mounts.cleanup_fstab([RESOURCE_DISK_PATH]) def clean(self): # Azure-specific cleanup logic for "cloud-init clean -c datasource" diff --git a/doc/rtd/reference/cli.rst b/doc/rtd/reference/cli.rst index 4b2cf178df1..dff40b50707 100644 --- a/doc/rtd/reference/cli.rst +++ b/doc/rtd/reference/cli.rst @@ -86,11 +86,15 @@ re-run all stages as it did on first boot. remove the file. Best practice when cloning a golden image, to ensure the next boot of that image auto-generates a unique machine ID. `More details on machine-id`_. -* :command:`--configs [all | ssh_config | network | datasource ]`: Optionally - remove all ``cloud-init`` generated config files. Argument `ssh_config` - cleans config files for ssh daemon. Argument `network` removes all generated - config files for network. Argument `datasource` removes files and/or configs - written by current datasource. `all` removes config files of all types. +* :command:`--configs [all | ssh_config | network | datasource | fstab ]`: + Optionally remove all ``cloud-init`` generated config files. Argument + `ssh_config` cleans config files for ssh daemon. Argument `network` removes + all generated config files for network. Argument `datasource` removes files + and/or configs written by current datasource. It includes `fstab` entries + that have been only configured by this datasource leaving aside other entries + configured by cloud-init. Argument `fstab` removes all entries that have + been configured by cloud-init including those that are configured by various + datasources. `all` removes config files of all types. * :command:`--seed`: Remove the cloud-init seed directory (e.g., :file:`/var/lib/cloud/seed/`) which stores instance metadata used initializing a datasource. diff --git a/tests/unittests/cmd/test_clean.py b/tests/unittests/cmd/test_clean.py index 5d6e4a19e8c..2f2b6c04bb4 100644 --- a/tests/unittests/cmd/test_clean.py +++ b/tests/unittests/cmd/test_clean.py @@ -7,6 +7,7 @@ import cloudinit.settings from cloudinit.cmd import clean +from cloudinit.config import cc_mounts from cloudinit.distros import Distro from cloudinit.sources import DataSource from cloudinit.stages import Init @@ -198,6 +199,75 @@ def test_keep_net_conf(self, clean_paths, init_class): assert conf_path.exists() is True, f"file {conf_path} removed!" assert 0 == retcode + @pytest.mark.usefixtures("fake_filesystem") + def test_clean_fstab(self, clean_paths, init_class): + """remove_config removed added entries in fstab when + `cloud-init clean -c fstab` is used. + """ + fstab_original_content = ( + "UUID=abc123 / ext4 defaults 0 0\n" + "/workspace /mnt " + "auto defaults,nofail,x-systemd.after=" + "cloud-init.service,_netdev,comment=cloudconfig 0 2\n" + ) + fstab_expected_content = "UUID=abc123 / ext4 defaults 0 0\n" + + etc_path = "/etc" + if not os.path.exists(etc_path): + os.makedirs(etc_path) + fstab_path = cc_mounts.FSTAB_PATH + with open(fstab_path, "w") as fd: + fd.write(fstab_original_content) + + clean.remove_artifacts( + init_class, + remove_logs=False, + remove_config=["fstab"], + ) + + with open(fstab_path, "r") as fd: + fstab_new_content = fd.read() + assert fstab_expected_content == fstab_new_content + + @pytest.mark.usefixtures("fake_filesystem") + def test_clean_fstab_for_all(self, clean_paths, init_class): + """remove_config removed added entries in fstab when + `cloud-init clean -c all` is used. + """ + fstab_original_content = ( + "UUID=abc123 / ext4 defaults 0 0\n" + "/workspace /mnt " + "auto defaults,nofail,x-systemd.after=" + "cloud-init.service,_netdev,comment=cloudconfig 0 2\n" + ) + fstab_expected_content = "UUID=abc123 / ext4 defaults 0 0\n" + TEST_GEN_SSH_CONFIG_FILES = [ + clean_paths.tmpdir.join(conf_file) + for conf_file in clean.GEN_SSH_CONFIG_FILES + ] + + etc_path = "/etc" + if not os.path.exists(etc_path): + os.makedirs(etc_path) + fstab_path = cc_mounts.FSTAB_PATH + with open(fstab_path, "w") as fd: + fd.write(fstab_original_content) + + with mock.patch( + "cloudinit.cmd.clean.GEN_SSH_CONFIG_FILES", + TEST_GEN_SSH_CONFIG_FILES, + ): + + clean.remove_artifacts( + init_class, + remove_logs=False, + remove_config=["all"], + ) + + with open(fstab_path, "r") as fd: + fstab_new_content = fd.read() + assert fstab_expected_content == fstab_new_content + def test_clean_datasource_conf_without_cache( self, clean_paths, init_class ): From 8f89f56044efa07c43ef604d1b8fe22821c3dbb5 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 12 Aug 2025 11:11:24 -0500 Subject: [PATCH 065/150] test: increase 2nd disk size for disk setup tests (#6376) We've seen some out of disk space errors. This increases it to just over 5M --- tests/integration_tests/modules/test_disk_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/modules/test_disk_setup.py b/tests/integration_tests/modules/test_disk_setup.py index 71a19eba76a..ce7b4cc816d 100644 --- a/tests/integration_tests/modules/test_disk_setup.py +++ b/tests/integration_tests/modules/test_disk_setup.py @@ -29,7 +29,7 @@ def setup_and_mount_lxd_disk(instance: LXDInstance): @pytest.fixture def create_disk(): - subp("dd if=/dev/zero of={} bs=64k count=40".format(DISK_PATH).split()) + subp("dd if=/dev/zero of={} bs=64k count=100".format(DISK_PATH).split()) yield os.remove(DISK_PATH) From 3fc9a0648aef6d7b18402b24280aa6adddb4360a Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 12 Aug 2025 10:45:36 -0600 Subject: [PATCH 066/150] test: support systemctl try-reload-or-restart messaging alternatives (#6377) systemd 251 changed response message text from reload-or-try-restart to try-reload-or-restart --- tests/integration_tests/modules/test_combined.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests/modules/test_combined.py b/tests/integration_tests/modules/test_combined.py index cae59519af3..9d216a2a555 100644 --- a/tests/integration_tests/modules/test_combined.py +++ b/tests/integration_tests/modules/test_combined.py @@ -26,7 +26,7 @@ OS_IMAGE_TYPE, PLATFORM, ) -from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU +from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU, NOBLE from tests.integration_tests.util import ( get_feature_flag_value, get_inactive_modules, @@ -264,9 +264,13 @@ def test_no_problems(self, class_client: IntegrationInstance): # Some minimal images may not have an installed rsyslog package # Test user-data doesn't provide install_rsyslog: true so expect # warnings when not installed. + if CURRENT_RELEASE < NOBLE: + operation_name = "reload-or-try-restart" + else: + operation_name = "try-reload-or-restart" require_warnings.append( - "Failed to try-reload-or-restart rsyslog.service:" - " Unit rsyslog.service not found." + f"Failed to {operation_name} rsyslog.service: Unit" + " rsyslog.service not found." ) # Set ignore_deprecations=True as test_deprecated_message covers this verify_clean_boot( From f21d266eb358b5591128db3b413a72c903cadac1 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 12 Aug 2025 13:08:59 -0500 Subject: [PATCH 067/150] fix: Ensure 822 template renders correctly on Debian (#6381) Fixes GH-6380 --- templates/sources.list.debian.deb822.tmpl | 2 +- tests/unittests/helpers.py | 10 +++ tests/unittests/test_render_template.py | 97 +++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/templates/sources.list.debian.deb822.tmpl b/templates/sources.list.debian.deb822.tmpl index bb286e6641b..08032634f0c 100644 --- a/templates/sources.list.debian.deb822.tmpl +++ b/templates/sources.list.debian.deb822.tmpl @@ -29,6 +29,6 @@ Signed-By: {{primary_key | default('/usr/share/keyrings/debian-archive-keyring.g ## Major bug fix updates produced after the final release of the distribution. Types: deb deb-src URIs: {{security}} -Suites: {{codename}}{% if codename in ('buster', 'stretch') %}/updates{% else %}-security{% endif %} +Suites: {{codename}}{% if codename in ('buster', 'stretch') %}/updates{% else %}-security{% endif +%} Components: main Signed-By: {{security_key | default(primary_key, true) | default('/usr/share/keyrings/debian-archive-keyring.gpg', true)}} diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 8510b04bb62..7e9eda8c1b8 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -475,6 +475,16 @@ def skipUnlessJinja(): return skipIf(not JINJA_AVAILABLE, "No jinja dependency present.") +@skipUnlessJinja() +def skipUnlessJinjaVersionGreaterThan(version=(0, 0, 0)): + import jinja2 + + return skipIf( + condition=tuple(map(int, jinja2.__version__.split("."))) < version, + reason=f"jinj2 version is less than {version}", + ) + + def skipIfJinja(): return skipIf(JINJA_AVAILABLE, "Jinja dependency present.") diff --git a/tests/unittests/test_render_template.py b/tests/unittests/test_render_template.py index a152a8437a7..7f8fc944429 100644 --- a/tests/unittests/test_render_template.py +++ b/tests/unittests/test_render_template.py @@ -6,6 +6,7 @@ from cloudinit import subp, templater, util from tests.helpers import cloud_init_project_dir +from tests.unittests.helpers import skipUnlessJinjaVersionGreaterThan # TODO(Look to align with tools.render-template or cloudinit.distos.OSFAMILIES) DISTRO_VARIANTS = [ @@ -124,3 +125,99 @@ def test_variant_sets_network_renderer_priority_in_cloud_cfg( system_cfg = util.load_yaml(stream.read()) assert renderers == system_cfg["system_info"]["network"]["renderers"] + + +EXPECTED_DEBIAN = """\ +deb testmirror testcodename main +deb-src testmirror testcodename main +deb testsecurity testcodename-security main +deb-src testsecurity testcodename-security main +deb testmirror testcodename-updates main +deb-src testmirror testcodename-updates main +deb testmirror testcodename-backports main +deb-src testmirror testcodename-backports main +""" + +EXPECTED_DEBIAN_DEB822 = """\ +Types: deb deb-src +URIs: testmirror +Suites: testcodename testcodename-updates testcodename-backports +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +Types: deb deb-src +URIs: testsecurity +Suites: testcodename-security +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +""" + +EXPECTED_UBUNTU = """\ +deb testmirror testcodename main restricted +deb testmirror testcodename-updates main restricted +deb testmirror testcodename universe +deb testmirror testcodename-updates universe +deb testmirror testcodename multiverse +deb testmirror testcodename-updates multiverse +deb testmirror testcodename-backports main restricted universe multiverse +deb testsecurity testcodename-security main restricted +deb testsecurity testcodename-security universe +deb testsecurity testcodename-security multiverse +""" + +EXPECTED_UBUNTU_DEB822 = """\ +Types: deb +URIs: testmirror +Suites: testcodename testcodename-updates testcodename-backports +Components: main universe restricted multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg +Types: deb +URIs: testsecurity +Suites: testcodename-security +Components: main universe restricted multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg +""" + + +class TestRenderSourcesList: + @pytest.mark.parametrize( + "template_path,expected", + [ + pytest.param( + "templates/sources.list.debian.tmpl", + EXPECTED_DEBIAN, + id="debian", + ), + pytest.param( + "templates/sources.list.debian.deb822.tmpl", + EXPECTED_DEBIAN_DEB822, + id="debian_822", + ), + pytest.param( + "templates/sources.list.ubuntu.tmpl", + EXPECTED_UBUNTU, + id="ubuntu", + ), + pytest.param( + "templates/sources.list.ubuntu.deb822.tmpl", + EXPECTED_UBUNTU_DEB822, + id="ubuntu_822", + ), + ], + ) + @skipUnlessJinjaVersionGreaterThan((3, 0, 0)) + def test_render_sources_list_templates( + self, tmpdir, template_path, expected + ): + params = { + "mirror": "testmirror", + "security": "testsecurity", + "codename": "testcodename", + } + template_path = cloud_init_project_dir(template_path) + rendered = templater.render_string(open(template_path).read(), params) + filtered = "\n".join( + line + for line in rendered.splitlines() + if line.strip() and not line.strip().startswith("#") + ) + assert filtered.strip() == expected.strip() From e682bef5be7f18d6c803fbc3ad35e277fdf03e39 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 12 Aug 2025 14:00:58 -0500 Subject: [PATCH 068/150] Release 25.2 Bump the version in cloudinit/version.py to 25.2 and update ChangeLog. --- ChangeLog | 227 +++++++++++++++++++++++++++++++++++++++++++ cloudinit/version.py | 2 +- 2 files changed, 228 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 9440238e223..a9f62d02140 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,230 @@ +25.2 + - fix: Ensure 822 template renders correctly on Debian (#6381) (GH: 6380) + - test: support systemctl try-reload-or-restart messaging alternatives + (#6377) + - test: increase 2nd disk size for disk setup tests (#6376) + - feat(clean): Add a new clean option to clean fstab entries (#6348) + [Ani Sinha] + - test: handle TMPDIR != "/tmp" (#6356) [Dan Bungert] + - fix: add openeuler hosts template to avoid cloud-init service failures + (#6328) [xqs] + - test: increase timeout on test_clean_package_install (#6362) + - test: relax checks on authorized keys (#6361) + - fix: Correct v2 ENI route and dns rendering (#6331) + - test: drop citestcase from i sources (#6346) + - test: drop citestcase from g and h sources (#6344) + - test: drop citestcase from c and d sources (#6336) + - test: drop citestcase from al* sources tests (#6318) + - disk_setup: Optionally use 'sfdisk' for GPT [Vitaly Kuznetsov] (GH: 5797) + - disk_setup: Fully support full GPT partition GUIDs [Vitaly Kuznetsov] + - fix(ENI): render keys with hyphens (#6333) (GH: 5234) + - fix: fixed defs types in cloudinit analyze dump (#6343) [abdulganiyy] + - feat(networkd): support vlan and bond rendering (#6324) + [Shreenidhi Shedi] + - fix(analyze-boot): use monotonic clock for containers #6322 + [Mostafa Abdelwahab] (GH: 5773) + - clean: do not attempt to find datasource when cache has been cleaned + (#6325) [Ani Sinha] + - test: drop citestcase from azure_helper tests (#6335) + - chore: remove unused target arg from Activator.available + - chore: add typing to cloudinit.net.eni + - feat(azure): Implement the clean callback for DataSourceAzure (#6321) + [Amy Chen] + - feat(oracle): set keep_configuration to true for iscsi instances [a-dubs] + - feat(network): add keep_configuration setting to v1 [a-dubs] + - doc: Document how to wait for cloud-init + - doc: Document changed systemd order in 24.4 + - fixed defs types in cloudinit analyze (#6308) [abdulganiyy] + - ci: downgrade LXD to fix integration tests (#6340) + - test: drop citestcase from azure source test (#6320) + - fix: catch and log exception during pip upgrade in ansible pull (#6301) + [Mostafa Abdelwahab] (GH: 6074) + - test: drop citestcase from some config tests + - fix: GCE datasource query issue (#3398) (#6279) + [ludovictual-system-u] (GH: 3398) + - test: drop citestcase from distro tests (#6317) + - test: drop citestcase from rest of config tests (#6314) + - ci: update patch checking to use current supported releases (#6315) + - test: drop FilesystemMockingTestCase (#6294) (GH: 5760) + - test: ensure image setup only runs once, even for xdist + - test: turn reaper into a fixture + - test: refactor obtaining session args into new function + - test: simplify passing of lxd_setup callback + - test: drop citestcase from analyze tests (#6312) + - test: drop FilesystemMockingTestCase in netconfig (#6290) + - test: expose tests.unittests.ditros._get_distro (#6290) + - doc: detailed examples of openstack config alternatives for non-x86 + images (#6303) + - test: drop FilesystemMockingTestCase in smartos (#6291) + - test: drop some FilesystemMockingTestCases (#6284) + - docs: clarify what #include can include (#6309) + - doc(ssdlc): Add hardening page (#6297) + - docs: Document network device table and limitations (#6187) + [Bryan Fraschetti] + - fix: allow downstreams to strip invalid MTU (#6246) (GH: 6239) + - test: drop FilesystemMockingTestCase from cc tests (#6282) + - docs: use correct jinja content-type (#6296) (GH: 6027) + - test: drop ResponsesTestCase (#6277) + - test: Consolidate use of paths fixture (#6289) + - test: pytestify test_vmware.py (#6288) [Mostafa Abdelwahab] + - feat(ca_certs): Add CentOS support (#6287) [Kees Bakker] + - feat(wsl): Reuse metadata as Landscape installation_request_id (#6200) + [Carlos Nihelton] + - test: drop / pytestify ResourceUsingTestCase (#6276) + - docs(mounts): Correct fs-freq reference to fs_freq (#6262) + [ibrahim-mojalled] (GH: 6210) + - test: pytestify ds_identify tests (#6274) + - test: pytestify t/u/test_log.py (#6275) + - feat(hetzner): integrate private networks metadata (#6224) + [Thomas Boerger] + - test: pytestify cloudinit.s.helpers.netlink tests (#6273) + - Release 25.1.4 + - Release 25.1.3 + - fix: strict disable in ds-identify on no datasources found (LP: #2069607) + - test: add integration test to install cloud-init from clean state (#6260) + - docs: provide example3 for PAM and ssh_pwauth behavior (#27) + - fix: Make hotplug socket writable only by root (#25) + - fix: Don't attempt to identify non-x86 OpenStack instances (LP: #2069607) + - doc: update discourse release schedule link (#6263) + - doc: update discourse link + - chore: log URL when retrying 503s (#6264) + - test: add option to keep instance on error (#6241) + - eni.py: ensure that a "dns" entry is not rendered in interfaces file + (#6253) [dermotbradley] + - feat(azure): improve handling for reading VM ID (#6199) [Chris Patterson] + - fix: remove unnecessary 'Wants' from cloud-init-main.service (#6255) + - test: use latest version of LXD in integration tests (#6249) + - docs: include missing --seed flags in clean CLI (#6244) [Faizan Alam] + - ec2: Improve metadata retrieval by iterating all interfaces (#6233) + [yukariatlas] (GH: 6232) + - feat: Add Raspberry Pi OS support (#5827) [Paul] + - feat(azure): add interface to dhcp_log_func (#6238) [Chris Patterson] + - fix: fix untyped-defs on tests/unittests and cloudinit/sources (#6230) + [Ritvikj23] + - test: pre-fetch instance id when logging from reaper (#6234) + - feat: Add subnet metric support for netplan (#6222) [Artsiom] + - test: remove FilesystemMockingTestCase from test_cc_apk_configure.py + (#6226) + - fix: make 'cloud-init --all-stages' work interactively (#6211) + - test: remove FilesystemMockingTestCase from test_util.py (#6220) + - test: remove FilesystemMockingTestCase from test_dmi.py (#6219) + - docs: merge "Availability" and "Supported distros" pages (#6217) + [Jacob C. Chin] + - fix: ansible-pull multiple playbooks on older ansible ver < 2.12 (#6218) + - docs: remove monospace formatting from SSH section heading (#6215) + [Chad Dougherty] + - fix: no traceback on command line missing subcommand (#6214) + [Robert Schweikert] + - feat: Change ansible pull module type from dict to list (#6010) + [Amirhossein Shaerpour] + - test(apt): add questing versiong for hello pkg (#6213) + - docs: Use Noble for examples instead of Jammy (#6209) [Aarni Koskela] + - feat(ca_certs): add rocky to rhel distro overrides (#6208) + [Lukas Friedhoff] + - docs: ensure proper 'sudo' representation (#6196) (GH: 6195) + - fix(net): ignore udevadm failures when enumerating nics (#6185) + [Chris Patterson] + - Release 25.1.2 (#6197) + - revert "chore: Deprecate partially supported system config (#5515)" + (LP: #2100232) + - fix: copr builds of CentOS9 require CRB and baseurl in centos.repo + (#6192) + - chore: add fedora package build support for run-container and read-deps + (#6174) + - test: drop fixed xfail tests + - fix: simplify MAAS check logic + - feat(azure): allow unspecified user name (#6177) [Chris Patterson] + - fix: stop reporting error if cloud-init receives signal (#6159) + (GH: 6151) + - Fix: Add Ephemeral Network for CloudStackLocal DS (#6144) + [Bryan Fraschetti] (GH: 6143) + - docs: clarify examples for network addresses/gateway (#6186) + [Dan Bungert] + - fix: setup.py doesn't match AmazonLinux CPE 2.2 releases (#6173) + - fix: ensure MAAS datasource retries on failure (#6167) (LP: #2106671) + - cloud.cfg.tmpl: do not enable cc_reset_rmc for Alpine Linux (#6170) + [dermotbradley] + - tests: ibm avoid schema validation for DataSourceNone on ibm (#6168) + - tests: ibm fix apt and ds-id testing (#6168) + - test: fix integration test on new lxd versions (#6164) + - tests: ibm correct logged message. Invalid schema: vendor-data (#6163) + - tests: ibm expect invalid vendor-data in stderr (#6160) + - tests: ibm expect invalid vendor-data in stderr (#6158) + - tests: fix ibm expected warnings on invalid vendor-data schema (#6157) + - fix: drop udev remove action in hotplug (#6152) [yukariatlas] + - chore: remove reference to refresh_rmc_and_interface module (#6156) + [Ani Sinha] + - chore: reorder iface filters & log on inherited MAC (#6140) + [Wesley Hershberger] + - fix(azure): update ReportableErrorUnhandledException (#6133) + [Ksenija Stanojevic] + - chore: make auditd wait for cloud-init-local.service (#6138) + [Robert Schweikert] + - chore: allow custom pkg-config binary path (#6118) + [Alexandre Burgoni] (GH: 6099) + - fix: rename "reload-or-try-restart" to "try-reload-or-restart" (#6142) + [sxt1001] + - chore: make lint interpreter configurable (#6121) [Robert Schweikert] + - fix: ensure system sshd_config is not overwritten (#6105) [Sludge] + - fix(oracle): properly detect ipv6 only for private ULA addresses (#6123) + [Alec Warren] + - feat(oracle): downgrade warning log about falling back to imds (#6134) + [Alec Warren] + - Release 25.1.1 (#6120) + - chore: remove remaining references to .github-cla-signers (#6116) + - fix(cli): wrong usage output when invalid arg in subcommand (#6115) + [Dillon] (GH: 4609) + - fix(Azure): don't reraise FileNotFoundError during ephemeral setup + (#6113) + - fix(azure): handle unexpected exceptions during obtain_lease() (#6092) + [Ksenija Stanojevic] + - feat: add callback for datasources to clean config changes (#6100) + [Ani Sinha] + - chore: Fix untyped-defs on tests/unittests/config (#6104) [Vlad Apostol] + - chore: switch to has-signed-canonical-cla GH action (#6109) + - Allow to set mac_address for VLAN subinterface (#6081) + [jumpojoy] (GH: 5364) + - change retry sleep for wireserver (#6107) [Ksenija Stanojevic] + - test: pytestify cc_chef tests, add migration test + - chef + - chef: migrate files in old config directories for backups and cache + - fix: correct the path for Chef's backups (#5994) + - test: replace version check with has_netplanlib() (#6106) + - feat(vmware): Support network events (#6063) [Andrew Kutz] + - test: correctly mock fallback nic in openstack tests (#6101) + - fix: Remove erroneous EC2 reference from 503 warning (#6077) + - test: update keys_to_console timeout (#6087) + - test: move to has_netplanlib() in test_networking.py (#6089) + - fix: NM reload and bring up individual network conns (#6073) [Ani Sinha] + - ci(oracle): fix issue installing cloud-init on custom image creation + (#6084) [Alec Warren] + - test: decouple netplan integrations from libnetplan SRU (#6085) + - test: warning on users/groups test is version specific (#6078) + - test: fix errors in custom datasource networking (#6076) + - test: ensure software-properties-common properly removed (#6080) + - test: remove script death check from test_signal_handler.py (#6079) + - test: ensure NoCloud networking works on plucky (#6072) + - fix: stop warning on dual-stack request failure (#6044) + - test: Add integration test for signal handling warnings/errors (#6037) + - feat(azure): format ReportableErrorImdsInvalidMetadata without repr + (#6052) [Ksenija Stanojevic] + - docs: v1.cloud_name section typo (#6070) [Jack Bernhardt] + - fix: install_method: pip cannot find ansible-pull command path (#6021) + [Hasan Aliyev] (GH: 5720) + - fix: Fix DataSourceAliYun exception_cb signature (#6068) (GH: 6066) + - fix: Update OauthUrlHelper to use readurl exception_cb signature + (GH: 6065) + - test: add OauthUrlHelper tests + - test: Remove CiTestCase from test_url_helper.py + - test: pytestify test_url_helper.py + - test: check for correct logrotate config (#6060) + - test: don't fail test if ppa has no uploads (#6059) + - test: make users/groups warning test release aware (#6056) + - fix: when get_session_cloud() fails, _SESSION_CLOUD isn't set (#6051) + - chore: Sort .gitignore + - chore: Add new entries to gitignore and glob more + - fix: track more removed modules (#6043) + 25.1.4 - fix: disable cloud-init when non-x86 environments have no DMI-data and no strict datasources detected (LP: #2069607) (CVE-2024-6174) diff --git a/cloudinit/version.py b/cloudinit/version.py index b0124088283..f6e8515952d 100644 --- a/cloudinit/version.py +++ b/cloudinit/version.py @@ -4,7 +4,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -__VERSION__ = "25.1.4" +__VERSION__ = "25.2" _PACKAGED_VERSION = "@@PACKAGED_VERSION@@" FEATURES = [ From 265fabd5f07784af0ee496f6ea3c83a8be62fa51 Mon Sep 17 00:00:00 2001 From: dermotbradley Date: Wed, 13 Aug 2025 22:22:47 +0100 Subject: [PATCH 069/150] cloud-init-hotplugd: remove use of "exec" and unnecessary exit (#6357) "exec" should not have been used to call cloud-init so remove it, also only call "cloud-init devel hotplug-hook" when there are arguments to pass to it. The "exit" at the end of the script is unnecessary, so remove it. Fixes GH-6351 --- tools/cloud-init-hotplugd | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/cloud-init-hotplugd b/tools/cloud-init-hotplugd index 3d56fffa7aa..eb811d69b10 100755 --- a/tools/cloud-init-hotplugd +++ b/tools/cloud-init-hotplugd @@ -11,13 +11,12 @@ PIPE="/run/cloud-init/share/hook-hotplug-cmd" -mkfifo -m700 $PIPE +[ -p $PIPE ] || mkfifo -m700 $PIPE while true; do # shellcheck disable=SC2162 - read args < $PIPE - # shellcheck disable=SC2086 - exec /usr/bin/cloud-init devel hotplug-hook $args + if read args < $PIPE; then + # shellcheck disable=SC2086 + /usr/bin/cloud-init devel hotplug-hook $args + fi done - -exit From dc0bfda399d5e8857dce6cd211f67b2740ac596f Mon Sep 17 00:00:00 2001 From: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:25:41 +0530 Subject: [PATCH 070/150] feat(networkd): add support for rendering bridge devices (#6349) This change extends the networkd renderer to support bridges defined through network config. Add tests for same. --- cloudinit/net/network_state.py | 1 + cloudinit/net/networkd.py | 250 ++++++++++++------ .../systemd/network/10-cloud-init-br0.netdev | 13 + .../systemd/network/10-cloud-init-br0.network | 19 ++ .../systemd/network/10-cloud-init-br1.netdev | 13 + .../systemd/network/10-cloud-init-br1.network | 19 ++ .../systemd/network/10-cloud-init-br2.netdev | 11 + .../systemd/network/10-cloud-init-br2.network | 13 + .../network/10-cloud-init-eth0.network | 28 ++ .../network/10-cloud-init-eth2.network | 16 ++ .../network/10-cloud-init-eth3.network | 10 + .../network/10-cloud-init-eth4.network | 10 + .../network/10-cloud-init-vl101.network | 1 + .../network/10-cloud-init-vlan100.network | 5 + .../network/10-cloud-init-vlan101.netdev | 7 + .../network/10-cloud-init-vlan101.network | 23 ++ .../network/10-cloud-init-vlan102.netdev | 7 + .../network/10-cloud-init-vlan102.network | 22 ++ .../net/artifacts/photon_net_config_v2.yaml | 168 ++++++++++-- 19 files changed, 537 insertions(+), 99 deletions(-) create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth3.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth4.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.network create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.netdev create mode 100644 tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.network diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index 9fd51e42916..0d250ecdcf6 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -923,6 +923,7 @@ def _add_dhcp_overrides(overrides, subnet): "gateway": route.get("via"), "metric": route.get("metric"), "mtu": route.get("mtu"), + "table": route.get("table"), } ) ) diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py index 8672e56e364..777490e6f6b 100644 --- a/cloudinit/net/networkd.py +++ b/cloudinit/net/networkd.py @@ -5,7 +5,8 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging -from typing import Any, Dict, List, Optional +from collections import defaultdict +from typing import Any, Callable, Dict, List, Optional, Tuple from cloudinit import subp, util from cloudinit.net import renderer, should_add_gateway_onlink_flag @@ -66,6 +67,8 @@ def __init__(self): "NetDev": [], "VLAN": [], "Bond": [], + "Bridge": [], + "RoutingPolicyRule": {}, } def update_section(self, sec, key, val): @@ -102,7 +105,7 @@ def get_final_conf(self): if k == "Address": for e in sorted(v): contents += f"[{k}]\n{e}\n\n" - elif k == "Route": + elif k in ["Route", "RoutingPolicyRule"]: for n in sorted(v): contents += f"[{k}]\n" for e in sorted(v[n]): @@ -179,6 +182,7 @@ def parse_routes(self, rid, conf, cfg: CfgParser): "gateway": "Gateway", "network": "Destination", "metric": "Metric", + "table": "Table", } # prefix is derived using netmask by network_state @@ -348,6 +352,7 @@ def render_network_state( network = self._render_content(network_state) vlan_netdev = network.pop("vlan_netdev", {}) bond_netdev = network.pop("bond_netdev", {}) + bridge_netdev = network.pop("bridge_netdev", {}) for k, v in network.items(): self.create_network_file(k, v, network_dir) @@ -358,15 +363,40 @@ def render_network_state( for k, v in bond_netdev.items(): self.create_network_file(k, v, network_dir, ext=".netdev") + for k, v in bridge_netdev.items(): + self.create_network_file(k, v, network_dir, ext=".netdev") + + def parseRoutingPolicy(self, policy, cfg: CfgParser): + rid = 0 + key = "RoutingPolicyRule" + for val in policy: + for k, v in val.items(): + cfg.update_route_section(key, rid, k.capitalize(), v) + rid += 1 + + def extractRoutingPolicies(self, nsCfg: Dict): + routingPolicies = {} + key = "routing-policy" + + for section in ("ethernets", "bonds", "vlans", "bridges"): + if section not in nsCfg: + continue + for iface, settings in nsCfg[section].items(): + if key in settings: + routingPolicies[iface] = settings[key] + return routingPolicies + def _render_content(self, ns: NetworkState): ret_dict = {} vlan_link = {} bond_link = {} + bridge_link = {} if "vlans" in ns.config: vlan_dict = self.render_vlans(ns) vlan_netdev = vlan_dict["vlan_netdev"] vlan_link = vlan_dict["vlan_link"] + vlan_link["macaddress"] = vlan_dict["vlan_mac_info"] ret_dict["vlan_netdev"] = vlan_netdev if "bonds" in ns.config: @@ -375,14 +405,26 @@ def _render_content(self, ns: NetworkState): bond_link = bond_dict["bond_link"] ret_dict["bond_netdev"] = bond_netdev + if "bridges" in ns.config: + bridge_dict = self.render_bridges(ns) + bridge_netdev = bridge_dict["bridge_netdev"] + bridge_link = bridge_dict["bridge_link"] + ret_dict["bridge_netdev"] = bridge_netdev + + routingPolicies = self.extractRoutingPolicies(ns.config) + for iface in ns.iter_interfaces(): cfg = CfgParser() iface_name = iface["name"] - vlan_link_name = vlan_link.get(iface_name) - if vlan_link_name: - cfg.update_section("Network", "VLAN", vlan_link_name) + vlan_link_name = vlan_link.get(iface_name, []) + for i in vlan_link_name: + cfg.update_section("Network", "VLAN", i) + + rPolicy = routingPolicies.get(iface_name) + if rPolicy: + self.parseRoutingPolicy(rPolicy, cfg) # TODO: revisit this once network state renders macaddress # properly for vlan config @@ -402,6 +444,33 @@ def _render_content(self, ns: NetworkState): if mac: iface["mac_address"] = mac + bridge_link_name = bridge_link.get(iface_name) + if bridge_link_name: + val = ( + bridge_link["path-cost"] + .get(bridge_link_name, {}) + .get(iface_name) + ) + if val: + cfg.update_section("Bridge", "Cost", val) + + val = ( + bridge_link["port-priority"] + .get(bridge_link_name, {}) + .get(iface_name) + ) + if val: + cfg.update_section("Bridge", "Priority", val) + + cfg.update_section("Network", "Bridge", bridge_link_name) + + # TODO: revisit this once network state renders macaddress + # properly for bridge config + if not iface["mac_address"] and bridge_link.get("macaddress"): + mac = bridge_link["macaddress"].get(iface_name) + if mac: + iface["mac_address"] = mac + link = self.generate_match_section(iface, cfg) self.generate_link_section(iface, cfg) @@ -457,9 +526,9 @@ def _render_content(self, ns: NetworkState): return ret_dict def render_vlans(self, ns: NetworkState) -> dict: - vlan_link_info: Dict[str, Any] = {} + vlan_link_info = defaultdict(list) + vlan_mac_info = {} vlan_ndev_configs = {} - vlan_link_info["macaddress"] = {} vlans = ns.config.get("vlans", {}) for vlan_name, vlan_cfg in vlans.items(): @@ -472,7 +541,7 @@ def render_vlans(self, ns: NetworkState) -> dict: ) continue - vlan_link_info[parent] = vlan_name + vlan_link_info[parent].append(vlan_name) # -------- .netdev for VLAN -------- cfg = CfgParser() @@ -487,16 +556,16 @@ def render_vlans(self, ns: NetworkState) -> dict: if val: val = val.lower() cfg.update_section("NetDev", "MACAddress", val) - vlan_link_info["macaddress"][vlan_name] = val + vlan_mac_info[vlan_name] = val cfg.update_section("VLAN", "Id", vlan_id) vlan_ndev_configs[vlan_name] = cfg.get_final_conf() - ret_dict = { + return { "vlan_netdev": vlan_ndev_configs, "vlan_link": vlan_link_info, + "vlan_mac_info": vlan_mac_info, } - return ret_dict def render_bonds(self, ns: NetworkState) -> dict: bond_link_info: Dict[str, Any] = {} @@ -534,89 +603,116 @@ def render_bonds(self, ns: NetworkState) -> dict: # Optional bond parameters params = bond_cfg.get("parameters", {}) - if "mode" in params: - cfg.update_section(section, "Mode", params["mode"]) - - if "mii-monitor-interval" in params: - cfg.update_section( - section, - "MIIMonitorSec", - f"{params['mii-monitor-interval']}ms", - ) - - if "updelay" in params: - cfg.update_section( - section, "UpDelaySec", f"{params['updelay']}ms" - ) + ParamMapType = Dict[str, Tuple[str, Callable[[object], str]]] - if "downdelay" in params: - cfg.update_section( - section, "DownDelaySec", f"{params['downdelay']}ms" - ) + param_map: ParamMapType = { + "ad-select": ("AdSelect", str), + "all-slaves-active": ( + "AllSlavesActive", + lambda v: str(v).lower(), + ), + "arp-all-targets": ("ARPAllTargets", str), + "arp-interval": ("ARPIntervalSec", lambda v: f"{v}ms"), + "arp-validate": ("ARPValidate", str), + "down-delay": ("DownDelaySec", lambda v: f"{v}ms"), + "fail-over-mac-policy": ("FailOverMACPolicy", str), + "gratuitous-arp": ("GratuitousARP", str), + "lacp-rate": ("LACPTransmitRate", str), + "learn-packet-interval": ("LearnPacketIntervalSec", str), + "mii-monitor-interval": ("MIIMonitorSec", lambda v: f"{v}ms"), + "min-links": ("MinLinks", lambda v: str(v)), + "mode": ("Mode", str), + "packets-per-slave": ("PacketsPerSlave", str), + "primary-reselect-policy": ("PrimaryReselectPolicy", str), + "transmit-hash-policy": ("TransmitHashPolicy", str), + "up-delay": ("UpDelaySec", lambda v: f"{v}ms"), + } - if "arp-interval" in params: - cfg.update_section( - section, "ARPIntervalSec", f"{params['arp-interval']}ms" - ) + for key, (option, formatter) in param_map.items(): + if key in params: + cfg.update_section(section, option, formatter(params[key])) - if "arp-ip-target" in params: - targets = params["arp-ip-target"] + if "arp-ip-targets" in params: + targets = params["arp-ip-targets"] if isinstance(targets, str): targets = [targets] - ip_list = " ".join(targets) - cfg.update_section(section, "ARPIPTargets", ip_list) + cfg.update_section(section, "ARPIPTargets", " ".join(targets)) - if "arp-validate" in params: - cfg.update_section( - section, "ARPValidate", params["arp-validate"] - ) + bond_ndev_configs[bond_name] = cfg.get_final_conf() - if "arp-all-targets" in params: - cfg.update_section( - section, "ARPAllTargets", params["arp-all-targets"] - ) + return { + "bond_netdev": bond_ndev_configs, + "bond_link": bond_link_info, + } - if "primary-reselect" in params: - cfg.update_section( - section, - "PrimaryReselectPolicy", - params["primary-reselect"], - ) + def render_bridges(self, ns: NetworkState) -> dict: + bridge_link_info: Dict[str, Any] = { + "macaddress": {}, + "path-cost": {}, + "port-priority": {}, + } - if "lacp-rate" in params: - cfg.update_section( - section, "LACPTransmitRate", params["lacp-rate"] - ) + bridge_ndev_configs = {} - if "transmit-hash-policy" in params: - cfg.update_section( - section, - "TransmitHashPolicy", - params["transmit-hash-policy"], + bridges = ns.config.get("bridges", {}) + for bridge_name, bridge_cfg in bridges.items(): + interfaces = bridge_cfg.get("interfaces", []) + if not interfaces: + LOG.warning( + "Skipping bridge %s - missing 'interfaces'", bridge_name ) + continue - if "ad-select" in params: - cfg.update_section(section, "AdSelect", params["ad-select"]) + # Map each interface to its bridge + for iface in interfaces: + bridge_link_info[iface] = bridge_name - if "min-links" in params: - cfg.update_section( - section, "MinLinks", str(params["min-links"]) - ) + # ---- .netdev config for the bridge ---- + cfg = CfgParser() + cfg.update_section("NetDev", "Name", bridge_name) + cfg.update_section("NetDev", "Kind", "bridge") - if "all-slaves-active" in params: - cfg.update_section( - section, - "AllSlavesActive", - str(params["all-slaves-active"]).lower(), - ) + val = bridge_cfg.get("mtu") + if val: + cfg.update_section("NetDev", "MTUBytes", val) - bond_ndev_configs[bond_name] = cfg.get_final_conf() + val = bridge_cfg.get("macaddress") + if val: + val = val.lower() + cfg.update_section("NetDev", "MACAddress", val) + bridge_link_info["macaddress"][bridge_name] = val + + # Bridge parameters + params = bridge_cfg.get("parameters", {}) + param_map = { + "ageing-time": "AgeingTimeSec", + "forward-delay": "ForwardDelaySec", + "hello-time": "HelloTimeSec", + "max-age": "MaxAgeSec", + "priority": "Priority", + "stp": "STP", + } + for key, sysd_key in param_map.items(): + val = params.get(key) + if val: + if isinstance(val, bool): + val = "yes" if val else "no" + cfg.update_section("Bridge", sysd_key, val) + + val = params.get("path-cost") + if val: + bridge_link_info["path-cost"][bridge_name] = val - ret_dict = { - "bond_netdev": bond_ndev_configs, - "bond_link": bond_link_info, + val = params.get("port-priority") + if val: + bridge_link_info["port-priority"][bridge_name] = val + + bridge_ndev_configs[bridge_name] = cfg.get_final_conf() + + return { + "bridge_netdev": bridge_ndev_configs, + "bridge_link": bridge_link_info, } - return ret_dict def available(target=None): diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.netdev new file mode 100644 index 00000000000..6240729bc99 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.netdev @@ -0,0 +1,13 @@ +[Bridge] +AgeingTimeSec=300 +ForwardDelaySec=4 +HelloTimeSec=2 +MaxAgeSec=20 +Priority=2048 +STP=yes + +[NetDev] +Kind=bridge +MACAddress=00:11:22:33:44:aa +MTUBytes=9000 +Name=br0 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.network new file mode 100644 index 00000000000..2b733945902 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br0.network @@ -0,0 +1,19 @@ +[Address] +Address=172.16.1.10/24 + +[Link] +MACAddress=00:11:22:33:44:aa +MTUBytes=9000 + +[Match] +Name=br0 + +[Network] +DHCP=no +DNS=1.1.1.1 +Domains=bridge.test + +[Route] +Destination=0.0.0.0/0 +Gateway=172.16.1.1 +Metric=100 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.netdev new file mode 100644 index 00000000000..6a7d7f81729 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.netdev @@ -0,0 +1,13 @@ +[Bridge] +AgeingTimeSec=200 +ForwardDelaySec=3 +HelloTimeSec=1 +MaxAgeSec=15 +Priority=4096 +STP=yes + +[NetDev] +Kind=bridge +MACAddress=00:11:22:33:44:88 +MTUBytes=1500 +Name=br1 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.network new file mode 100644 index 00000000000..ea656764107 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br1.network @@ -0,0 +1,19 @@ +[Address] +Address=10.0.0.10/24 + +[Link] +MACAddress=00:11:22:33:44:88 +MTUBytes=1500 + +[Match] +Name=br1 + +[Network] +DHCP=no +DNS=9.9.9.9 +Domains=vlan.bridge + +[Route] +Destination=10.0.0.0/24 +Gateway=10.0.0.1 +Metric=200 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.netdev new file mode 100644 index 00000000000..a273afde4b6 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.netdev @@ -0,0 +1,11 @@ +[Bridge] +AgeingTimeSec=100 +HelloTimeSec=2 +MaxAgeSec=20 +Priority=8192 + +[NetDev] +Kind=bridge +MACAddress=00:11:22:33:44:bb +MTUBytes=1600 +Name=br2 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.network new file mode 100644 index 00000000000..02b421b7c41 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-br2.network @@ -0,0 +1,13 @@ +[Address] +Address=192.168.50.10/24 + +[Link] +MACAddress=00:11:22:33:44:bb +MTUBytes=1600 + +[Match] +Name=br2 + +[Network] +DHCP=no +DNS=8.8.4.4 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network index e3a8358fd7a..16ea4e32dfb 100644 --- a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth0.network @@ -1,3 +1,6 @@ +[Address] +Address=192.0.2.10/24 + [Link] MTUBytes=1500 @@ -9,3 +12,28 @@ Name=eth0 Bond=bond0 DHCP=no VLAN=vlan100 +VLAN=vlan101 +VLAN=vlan102 + +[Route] +Destination=0.0.0.0/0 +Gateway=192.168.14.1 +Metric=50 +Table=10 + +[Route] +Destination=::/0 +Gateway=2001:1::2 +Metric=100 + +[RoutingPolicyRule] +From=192.0.2.10 +Priority=100 +Table=10 +To=203.0.113.100 + +[RoutingPolicyRule] +From=203.0.113.101/24 +Priority=90 +Table=70 +To=192.0.2.11/8 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network index 712c52b71b6..e637cbec9f7 100644 --- a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth2.network @@ -14,3 +14,19 @@ DHCP=no DNS=9.9.9.9 8.8.8.8 Domains=example.org VLAN=vl101 + +[Route] +Destination=198.51.100.0/24 +Gateway=10.20.30.1 +Metric=60 + +[Route] +Destination=192.0.2.0/24 +Gateway=10.20.30.1 +Metric=110 + +[RoutingPolicyRule] +From=10.20.30.40 +Priority=120 +Table=12 +To=198.51.100.200 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth3.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth3.network new file mode 100644 index 00000000000..149bb852027 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth3.network @@ -0,0 +1,10 @@ +[Bridge] +Cost=30 +Priority=32 + +[Match] +Name=eth3 + +[Network] +Bridge=br0 +DHCP=no diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth4.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth4.network new file mode 100644 index 00000000000..fb31f7dfc25 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-eth4.network @@ -0,0 +1,10 @@ +[Bridge] +Cost=40 +Priority=60 + +[Match] +Name=eth4 + +[Network] +Bridge=br0 +DHCP=no diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network index fb82925b4e6..2d9b2b6d947 100644 --- a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vl101.network @@ -2,4 +2,5 @@ Name=vl101 [Network] +Bridge=br2 DHCP=no diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network index f8afc4724f9..49ea2ce7d3c 100644 --- a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan100.network @@ -4,6 +4,10 @@ Address=192.168.100.10/24 [Address] Address=192.168.100.11/24 +[Bridge] +Cost=45 +Priority=50 + [Link] MACAddress=00:11:22:33:44:66 MTUBytes=901 @@ -12,6 +16,7 @@ MTUBytes=901 Name=vlan100 [Network] +Bridge=br1 DHCP=ipv6 DNS=8.8.8.8 1.1.1.1 Domains=corp.example.com vlan.test diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.netdev new file mode 100644 index 00000000000..aec56f8196b --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.netdev @@ -0,0 +1,7 @@ +[NetDev] +Kind=vlan +MTUBytes=1400 +Name=vlan101 + +[VLAN] +Id=101 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.network new file mode 100644 index 00000000000..ac9ff4189c5 --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan101.network @@ -0,0 +1,23 @@ +[Address] +Address=10.100.101.10/24 + +[Link] +MTUBytes=1400 + +[Match] +Name=vlan101 + +[Network] +DHCP=no +DNS=1.1.1.1 9.9.9.9 +Domains=vlan101.local + +[Route] +Destination=0.0.0.0/0 +Gateway=10.100.101.1 +Table=101 + +[RoutingPolicyRule] +From=10.100.101.10 +Priority=100 +Table=101 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.netdev b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.netdev new file mode 100644 index 00000000000..2d8e8fba03f --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.netdev @@ -0,0 +1,7 @@ +[NetDev] +Kind=vlan +MTUBytes=1400 +Name=vlan102 + +[VLAN] +Id=102 diff --git a/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.network b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.network new file mode 100644 index 00000000000..65b3f75412c --- /dev/null +++ b/tests/unittests/net/artifacts/photon_net_config/etc/systemd/network/10-cloud-init-vlan102.network @@ -0,0 +1,22 @@ +[Address] +Address=10.100.102.10/24 + +[Link] +MTUBytes=1400 + +[Match] +Name=vlan102 + +[Network] +DHCP=no +DNS=8.8.8.8 1.0.0.1 +Domains=vlan102.local + +[Route] +Destination=0.0.0.0/0 +Gateway=10.100.102.1 +Table=102 + +[RoutingPolicyRule] +From=10.100.102.10 +Table=102 diff --git a/tests/unittests/net/artifacts/photon_net_config_v2.yaml b/tests/unittests/net/artifacts/photon_net_config_v2.yaml index 7984f8438fe..7701158edde 100644 --- a/tests/unittests/net/artifacts/photon_net_config_v2.yaml +++ b/tests/unittests/net/artifacts/photon_net_config_v2.yaml @@ -8,6 +8,24 @@ network: set-name: eth0 dhcp4: false mtu: 1500 + addresses: [192.0.2.10/24] + routes: + - to: 0.0.0.0/0 + via: 192.168.14.1 + metric: 50 + table: 10 + - to: ::/0 + via: 2001:1::2 + metric: 100 + routing-policy: + - from: 192.0.2.10 + to: 203.0.113.100 + table: 10 + priority: 100 + - to: 192.0.2.11/8 + from: 203.0.113.101/24 + table: 70 + priority: 90 eth1: dhcp4: true @@ -17,16 +35,33 @@ network: match: macaddress: "00:aa:bb:cc:dd:ee" set-name: eth2 - mtu: 1400 dhcp4: false + mtu: 1400 addresses: - 10.20.30.40/24 nameservers: - addresses: - - 9.9.9.9 - - 8.8.8.8 - search: - - example.org + addresses: [9.9.9.9, 8.8.8.8] + search: [example.org] + routes: + - to: 198.51.100.0/24 + via: 10.20.30.1 + metric: 60 + - to: 192.0.2.0/24 + via: 10.20.30.1 + metric: 110 + routing-policy: + - from: 10.20.30.40 + to: 198.51.100.200 + table: 12 + priority: 120 + + eth3: + dhcp4: false + dhcp6: false + + eth4: + dhcp4: false + dhcp6: false bonds: bond0: @@ -45,19 +80,13 @@ network: vlan100: id: 100 link: eth0 - addresses: - - 192.168.100.10/24 - - 192.168.100.11/24 + addresses: [192.168.100.10/24, 192.168.100.11/24] dhcp6: true mtu: 901 macaddress: "00:11:22:33:44:66" nameservers: - addresses: - - 8.8.8.8 - - 1.1.1.1 - search: - - corp.example.com - - vlan.test + addresses: [8.8.8.8, 1.1.1.1] + search: [corp.example.com, vlan.test] routes: - to: 10.10.200.0/24 via: 192.168.100.1 @@ -66,6 +95,39 @@ network: via: 192.168.100.2 metric: 150 + vlan101: + id: 101 + link: eth0 + addresses: [10.100.101.10/24] + routes: + - to: 0.0.0.0/0 + via: 10.100.101.1 + table: 101 + routing-policy: + - from: 10.100.101.10 + table: 101 + priority: 100 + mtu: 1400 + nameservers: + addresses: [1.1.1.1, 9.9.9.9] + search: [vlan101.local] + + vlan102: + id: 102 + link: eth0 + addresses: [10.100.102.10/24] + routes: + - to: 0.0.0.0/0 + via: 10.100.102.1 + table: 102 + routing-policy: + - from: 10.100.102.10 + table: 102 + mtu: 1400 + nameservers: + addresses: [8.8.8.8, 1.0.0.1] + search: [vlan102.local] + vl101: id: 101 link: eth2 @@ -74,13 +136,75 @@ network: id: 200 link: bond0 macaddress: "00:11:22:33:44:99" - addresses: - - 192.168.200.10/24 + addresses: [192.168.200.10/24] nameservers: - addresses: - - 1.1.1.1 - - 8.8.4.4 - search: - - bond.vlan.test + addresses: [1.1.1.1, 8.8.4.4] + search: [bond.vlan.test] mtu: 111 dhcp6: true + + bridges: + br0: + interfaces: [eth3, eth4] + addresses: [172.16.1.10/24] + routes: + - to: 0.0.0.0/0 + via: 172.16.1.1 + metric: 100 + nameservers: + addresses: [1.1.1.1] + search: [bridge.test] + mtu: 9000 + macaddress: "00:11:22:33:44:aa" + parameters: + stp: true + forward-delay: 4 + hello-time: 2 + max-age: 20 + priority: 2048 + ageing-time: 300 + path-cost: + eth3: 30 + eth4: 40 + port-priority: + eth3: 32 + eth4: 60 + + br1: + interfaces: [vlan100] + addresses: [10.0.0.10/24] + routes: + - to: 10.0.0.0/24 + via: 10.0.0.1 + metric: 200 + nameservers: + addresses: [9.9.9.9] + search: [vlan.bridge] + mtu: 1500 + macaddress: "00:11:22:33:44:88" + parameters: + stp: true + forward-delay: 3 + hello-time: 1 + max-age: 15 + priority: 4096 + ageing-time: 200 + path-cost: + vlan100: 45 + port-priority: + vlan100: 50 + + br2: + interfaces: [vl101] + addresses: [192.168.50.10/24] + nameservers: + addresses: [8.8.4.4] + mtu: 1600 + macaddress: "00:11:22:33:44:BB" + parameters: + stp: false + forward-delay: 0 + hello-time: 2 + max-age: 20 + priority: 8192 + ageing-time: 100 From ab2a3365990665f8de75e28f4fbbeedf615873b4 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Thu, 14 Aug 2025 09:07:53 -0500 Subject: [PATCH 071/150] test: skip gpg dependency test on questing (#6388) The test involves removal of Ubuntu core packages. On Questing, this results in the gpg package being in a broken state that cannot be recovered by simply installing gnupg. --- .../modules/test_apt_functionality.py | 11 ++++++++++- tests/integration_tests/releases.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests/modules/test_apt_functionality.py b/tests/integration_tests/modules/test_apt_functionality.py index dd068d04b19..c7878cdd6d9 100644 --- a/tests/integration_tests/modules/test_apt_functionality.py +++ b/tests/integration_tests/modules/test_apt_functionality.py @@ -14,7 +14,12 @@ KEEP_INSTANCE, PLATFORM, ) -from tests.integration_tests.releases import CURRENT_RELEASE, IS_UBUNTU, MANTIC +from tests.integration_tests.releases import ( + CURRENT_RELEASE, + IS_UBUNTU, + MANTIC, + QUESTING, +) from tests.integration_tests.util import ( get_feature_flag_value, verify_clean_boot, @@ -544,6 +549,10 @@ def _do_oci_customization(cloud_config: str): @pytest.mark.skipif(not IS_UBUNTU, reason="Apt usage") +@pytest.mark.skipif( + CURRENT_RELEASE == QUESTING, + reason="Trying to remove gpg on Questing makes apt unhappy", +) def test_install_missing_deps(session_cloud: IntegrationCloud): """ Test the installation of missing dependencies using apt on an Ubuntu diff --git a/tests/integration_tests/releases.py b/tests/integration_tests/releases.py index 9f6c8ae7df5..3c3c1fd3df0 100644 --- a/tests/integration_tests/releases.py +++ b/tests/integration_tests/releases.py @@ -99,6 +99,7 @@ def from_os_image( NOBLE = Release("ubuntu", "noble", "24.04") ORACULAR = Release("ubuntu", "oracular", "24.10") PLUCKY = Release("ubuntu", "plucky", "25.04") +QUESTING = Release("ubuntu", "questing", "25.10") UBUNTU_STABLE = (FOCAL, JAMMY, MANTIC, NOBLE) From dbc63b03093e3f8669f063f8b93b3c75b6dfb6dd Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 14 Aug 2025 10:13:41 -0400 Subject: [PATCH 072/150] fix: make tools/tox-venv with current versions of tox (#6365) At some point in the past, 'tox --showconfig' would output 'envdir' for the directory of the virtual environment. current versions use `env_dir`. The change here makes ./tools/tox-venv work as it expects. Now './tools/tox-venv --list' will show the environments available. $ ./tools/tox-venv --list py3* black ruff isort mypy pylint --- tools/tox-venv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tox-venv b/tools/tox-venv index e16f129d226..c22f6faca4d 100755 --- a/tools/tox-venv +++ b/tools/tox-venv @@ -22,7 +22,7 @@ get_env_dirs() { key=${key%%=*} val=${equal} fi - [ "$key" = "envdir" ] || continue + [ "$key" = "envdir" ] || [ "$key" = "env_dir" ] || continue out="${out:+${out}${CR}}${curenv}:$val" done echo "$out" From 8617730558afbc324a7e54bff57a8c1062d44c28 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 14 Aug 2025 17:12:39 +0200 Subject: [PATCH 073/150] test: drop citestcase from n and o sources (#6363) Co-authored-by: Chad Smith --- tests/unittests/sources/test_nwcs.py | 49 ++- tests/unittests/sources/test_opennebula.py | 341 +++++++--------- tests/unittests/sources/test_openstack.py | 437 +++++++++------------ tests/unittests/sources/test_ovf.py | 304 +++++++------- 4 files changed, 507 insertions(+), 624 deletions(-) diff --git a/tests/unittests/sources/test_nwcs.py b/tests/unittests/sources/test_nwcs.py index 9ecd13a52aa..c208c875974 100644 --- a/tests/unittests/sources/test_nwcs.py +++ b/tests/unittests/sources/test_nwcs.py @@ -1,8 +1,10 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import helpers, settings, util +import pytest + +from cloudinit import settings, util from cloudinit.sources import DataSourceNWCS -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock METADATA = util.load_yaml( """ @@ -26,22 +28,18 @@ ) -class TestDataSourceNWCS(CiTestCase): +class TestDataSourceNWCS: """ Test reading the metadata """ - def setUp(self): - super(TestDataSourceNWCS, self).setUp() - self.tmp = self.tmp_dir() - - def get_ds(self): + @pytest.fixture + def ds(self, paths, tmp_path): distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - ds = DataSourceNWCS.DataSourceNWCS( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) + distro.get_tmp_exec_path = str(tmp_path) + return DataSourceNWCS.DataSourceNWCS( + settings.CFG_BUILTIN, distro, paths ) - return ds @mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") @mock.patch("cloudinit.sources.DataSourceNWCS.EphemeralDHCPv4") @@ -55,6 +53,7 @@ def test_read_data( m_fallback_nic, m_net, m_dhcp, + ds, ): m_ds_detect.return_value = True m_readmd.return_value = METADATA.copy() @@ -69,9 +68,7 @@ def test_read_data( } ] - ds = self.get_ds() - ret = ds.get_data() - self.assertTrue(ret) + assert ds.get_data() m_net.assert_called_once_with( ds.distro, @@ -81,29 +78,27 @@ def test_read_data( ], ) - self.assertTrue(m_readmd.called) + assert m_readmd.called - self.assertEqual(METADATA.get("hostname"), ds.get_hostname().hostname) + assert METADATA.get("hostname") == ds.get_hostname().hostname - self.assertEqual(METADATA.get("public-keys"), ds.get_public_ssh_keys()) + assert METADATA.get("public-keys") == ds.get_public_ssh_keys() - self.assertIsInstance(ds.get_public_ssh_keys(), list) - self.assertEqual(ds.get_userdata_raw(), METADATA.get("userdata")) - self.assertEqual(ds.get_vendordata_raw(), METADATA.get("vendordata")) + assert isinstance(ds.get_public_ssh_keys(), list) + assert ds.get_userdata_raw() == METADATA.get("userdata") + assert ds.get_vendordata_raw() == METADATA.get("vendordata") @mock.patch("cloudinit.sources.DataSourceNWCS.read_metadata") @mock.patch("cloudinit.net.find_fallback_nic") @mock.patch("cloudinit.sources.DataSourceNWCS.DataSourceNWCS.ds_detect") def test_not_on_nwcs_returns_false( - self, m_ds_detect, m_find_fallback, m_read_md + self, m_ds_detect, m_find_fallback, m_read_md, ds ): """If 'ds_detect' returns False, return False from get_data.""" m_ds_detect.return_value = False - ds = self.get_ds() - ret = ds.get_data() + assert not ds.get_data() - self.assertFalse(ret) # These are a white box attempt to ensure it did not search. m_find_fallback.assert_not_called() m_read_md.assert_not_called() @@ -112,6 +107,6 @@ def test_not_on_nwcs_returns_false( def test_get_interface_name(self, m_ifname): m_ifname.return_value = "eth0" - self.assertEqual( - m_ifname.return_value, METADATA["network"]["config"][0]["name"] + assert ( + m_ifname.return_value == METADATA["network"]["config"][0]["name"] ) diff --git a/tests/unittests/sources/test_opennebula.py b/tests/unittests/sources/test_opennebula.py index ecb0fa76b85..317a842bc89 100644 --- a/tests/unittests/sources/test_opennebula.py +++ b/tests/unittests/sources/test_opennebula.py @@ -2,13 +2,12 @@ import os import pwd -import unittest import pytest -from cloudinit import atomic_helper, helpers, util +from cloudinit import atomic_helper, util from cloudinit.sources import DataSourceOpenNebula as ds -from tests.unittests.helpers import CiTestCase, mock, populate_dir +from tests.unittests.helpers import mock, populate_dir TEST_VARS = { "VAR1": "single", @@ -41,20 +40,14 @@ DS_PATH = "cloudinit.sources.DataSourceOpenNebula" -class TestOpenNebulaDataSource(CiTestCase): +@pytest.mark.allow_subp_for("bash", "sh") +class TestOpenNebulaDataSource: parsed_user = None - allowed_subp = ["bash", "sh"] - - def setUp(self): - super(TestOpenNebulaDataSource, self).setUp() - self.tmp = self.tmp_dir() - self.paths = helpers.Paths( - {"cloud_dir": self.tmp, "run_dir": self.tmp} - ) + @pytest.fixture(autouse=True) + def fixtures(self, paths): # defaults for few tests - self.ds = ds.DataSourceOpenNebula - self.seed_dir = os.path.join(self.paths.seed_dir, "opennebula") + self.seed_dir = os.path.join(paths.seed_dir, "opennebula") self.sys_cfg = {"datasource": {"OpenNebula": {"dsmode": "local"}}} # we don't want 'sudo' called in tests. so we patch switch_user_cmd @@ -64,152 +57,129 @@ def my_switch_user_cmd(user): self.switch_user_cmd_real = ds.switch_user_cmd ds.switch_user_cmd = my_switch_user_cmd - - def tearDown(self): + yield ds.switch_user_cmd = self.switch_user_cmd_real - super().tearDown() - - def test_get_data_non_contextdisk(self): - orig_find_devs_with = util.find_devs_with - try: - # dont' try to lookup for CDs - util.find_devs_with = lambda n: [] # type: ignore - dsrc = self.ds(sys_cfg=self.sys_cfg, distro=None, paths=self.paths) - ret = dsrc.get_data() - self.assertFalse(ret) - finally: - util.find_devs_with = orig_find_devs_with - - def test_get_data_broken_contextdisk(self): - orig_find_devs_with = util.find_devs_with - try: - # dont' try to lookup for CDs - util.find_devs_with = lambda n: [] # type: ignore - populate_dir(self.seed_dir, {"context.sh": INVALID_CONTEXT}) - dsrc = self.ds(sys_cfg=self.sys_cfg, distro=None, paths=self.paths) - self.assertRaises(ds.BrokenContextDiskDir, dsrc.get_data) - finally: - util.find_devs_with = orig_find_devs_with - - def test_get_data_invalid_identity(self): - orig_find_devs_with = util.find_devs_with - try: - # generate non-existing system user name - sys_cfg = self.sys_cfg - invalid_user = "invalid" - while not sys_cfg["datasource"]["OpenNebula"].get("parseuser"): - try: - pwd.getpwnam(invalid_user) - invalid_user += "X" - except KeyError: - sys_cfg["datasource"]["OpenNebula"][ - "parseuser" - ] = invalid_user - - # dont' try to lookup for CDs - util.find_devs_with = lambda n: [] # type: ignore - populate_context_dir(self.seed_dir, {"KEY1": "val1"}) - dsrc = self.ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) - self.assertRaises(ds.BrokenContextDiskDir, dsrc.get_data) - finally: - util.find_devs_with = orig_find_devs_with - - def test_get_data(self): - orig_find_devs_with = util.find_devs_with - try: - # dont' try to lookup for CDs - util.find_devs_with = lambda n: [] # type: ignore - populate_context_dir(self.seed_dir, {"KEY1": "val1"}) - dsrc = self.ds(sys_cfg=self.sys_cfg, distro=None, paths=self.paths) - with mock.patch(DS_PATH + ".pwd.getpwnam") as getpwnam: - ret = dsrc.get_data() - self.assertEqual([mock.call("nobody")], getpwnam.call_args_list) - self.assertTrue(ret) - finally: - util.find_devs_with = orig_find_devs_with - self.assertEqual("opennebula", dsrc.cloud_name) - self.assertEqual("opennebula", dsrc.platform_type) - self.assertEqual( - "seed-dir (%s/seed/opennebula)" % self.tmp, dsrc.subplatform + + @pytest.fixture + def dsrc(self, paths): + return ds.DataSourceOpenNebula( + sys_cfg=self.sys_cfg, distro=None, paths=paths ) + # dont' try to lookup for CDs + @mock.patch(DS_PATH + ".util.find_devs_with", return_value=[]) + def test_get_data_non_contextdisk(self, m_find_devs_with, dsrc): + assert not dsrc.get_data() + + # dont' try to lookup for CDs + @mock.patch(DS_PATH + ".util.find_devs_with", return_value=[]) + def test_get_data_broken_contextdisk(self, m_find_devs_with, dsrc): + populate_dir(self.seed_dir, {"context.sh": INVALID_CONTEXT}) + with pytest.raises(ds.BrokenContextDiskDir): + dsrc.get_data() + + # dont' try to lookup for CDs + @mock.patch(DS_PATH + ".util.find_devs_with", return_value=[]) + def test_get_data_invalid_identity(self, m_find_devs_with, dsrc): + # generate non-existing system user name + sys_cfg = self.sys_cfg + invalid_user = "invalid" + while not sys_cfg["datasource"]["OpenNebula"].get("parseuser"): + try: + pwd.getpwnam(invalid_user) + invalid_user += "X" + except KeyError: + sys_cfg["datasource"]["OpenNebula"]["parseuser"] = invalid_user + + populate_context_dir(self.seed_dir, {"KEY1": "val1"}) + with pytest.raises(ds.BrokenContextDiskDir): + dsrc.get_data() + + # dont' try to lookup for CDs + @mock.patch(DS_PATH + ".util.find_devs_with", return_value=[]) + def test_get_data(self, m_find_devs_with, dsrc, paths): + populate_context_dir(self.seed_dir, {"KEY1": "val1"}) + with mock.patch(DS_PATH + ".pwd.getpwnam") as getpwnam: + ret = dsrc.get_data() + assert [mock.call("nobody")] == getpwnam.call_args_list + assert ret + assert "opennebula" == dsrc.cloud_name + assert "opennebula" == dsrc.platform_type + assert "seed-dir (%s/opennebula)" % paths.seed_dir == dsrc.subplatform + def test_seed_dir_non_contextdisk(self): - self.assertRaises( - ds.NonContextDiskDir, - ds.read_context_disk_dir, - self.seed_dir, - mock.Mock(), - ) + with pytest.raises(ds.NonContextDiskDir): + ds.read_context_disk_dir( + self.seed_dir, + mock.Mock(), + ) def test_seed_dir_empty1_context(self): populate_dir(self.seed_dir, {"context.sh": ""}) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertIsNone(results["userdata"]) - self.assertEqual(results["metadata"], {}) + assert results["userdata"] is None + assert results["metadata"] == {} def test_seed_dir_empty2_context(self): populate_context_dir(self.seed_dir, {}) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertIsNone(results["userdata"]) - self.assertEqual(results["metadata"], {}) + assert results["userdata"] is None + assert results["metadata"] == {} def test_seed_dir_broken_context(self): populate_dir(self.seed_dir, {"context.sh": INVALID_CONTEXT}) - self.assertRaises( - ds.BrokenContextDiskDir, - ds.read_context_disk_dir, - self.seed_dir, - mock.Mock(), - ) + with pytest.raises(ds.BrokenContextDiskDir): + ds.read_context_disk_dir( + self.seed_dir, + mock.Mock(), + ) def test_context_parser(self): populate_context_dir(self.seed_dir, TEST_VARS) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("metadata" in results) - self.assertEqual(TEST_VARS, results["metadata"]) + assert "metadata" in results + assert TEST_VARS == results["metadata"] - def test_ssh_key(self): + def test_ssh_key(self, tmp_path): public_keys = ["first key", "second key"] for c in range(4): for k in ("SSH_KEY", "SSH_PUBLIC_KEY"): - my_d = os.path.join(self.tmp, "%s-%i" % (k, c)) + my_d = str(tmp_path / f"{k}-{c}") populate_context_dir(my_d, {k: "\n".join(public_keys)}) results = ds.read_context_disk_dir(my_d, mock.Mock()) - self.assertTrue("metadata" in results) - self.assertTrue("public-keys" in results["metadata"]) - self.assertEqual( - public_keys, results["metadata"]["public-keys"] - ) + assert "metadata" in results + assert "public-keys" in results["metadata"] + assert public_keys == results["metadata"]["public-keys"] public_keys.append(SSH_KEY % (c + 1,)) - def test_user_data_plain(self): + def test_user_data_plain(self, tmp_path): for k in ("USER_DATA", "USERDATA"): - my_d = os.path.join(self.tmp, k) + my_d = os.path.join(tmp_path, k) populate_context_dir(my_d, {k: USER_DATA, "USERDATA_ENCODING": ""}) results = ds.read_context_disk_dir(my_d, mock.Mock()) - self.assertTrue("userdata" in results) - self.assertEqual(USER_DATA, results["userdata"]) + assert "userdata" in results + assert USER_DATA == results["userdata"] - def test_user_data_encoding_required_for_decode(self): + def test_user_data_encoding_required_for_decode(self, tmp_path): b64userdata = atomic_helper.b64e(USER_DATA) for k in ("USER_DATA", "USERDATA"): - my_d = os.path.join(self.tmp, k) + my_d = str(tmp_path / k) populate_context_dir(my_d, {k: b64userdata}) results = ds.read_context_disk_dir(my_d, mock.Mock()) - self.assertTrue("userdata" in results) - self.assertEqual(b64userdata, results["userdata"]) + assert "userdata" in results + assert b64userdata == results["userdata"] - def test_user_data_base64_encoding(self): + def test_user_data_base64_encoding(self, tmp_path): for k in ("USER_DATA", "USERDATA"): - my_d = os.path.join(self.tmp, k) + my_d = str(tmp_path / k) populate_context_dir( my_d, { @@ -219,11 +189,11 @@ def test_user_data_base64_encoding(self): ) results = ds.read_context_disk_dir(my_d, mock.Mock()) - self.assertTrue("userdata" in results) - self.assertEqual(USER_DATA, results["userdata"]) + assert "userdata" in results + assert USER_DATA == results["userdata"] @mock.patch(DS_PATH + ".get_physical_nics_by_mac") - def test_hostname(self, m_get_phys_by_mac): + def test_hostname(self, m_get_phys_by_mac, tmp_path): for dev in ("eth0", "ens3"): m_get_phys_by_mac.return_value = {MACADDR: dev} for k in ( @@ -233,15 +203,13 @@ def test_hostname(self, m_get_phys_by_mac): "IP_PUBLIC", "ETH0_IP", ): - my_d = os.path.join(self.tmp, k) + my_d = str(tmp_path / k) populate_context_dir(my_d, {k: PUBLIC_IP}) results = ds.read_context_disk_dir(my_d, mock.Mock()) - self.assertTrue("metadata" in results) - self.assertTrue("local-hostname" in results["metadata"]) - self.assertEqual( - PUBLIC_IP, results["metadata"]["local-hostname"] - ) + assert "metadata" in results + assert "local-hostname" in results["metadata"] + assert PUBLIC_IP == results["metadata"]["local-hostname"] @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_network_interfaces(self, m_get_phys_by_mac): @@ -253,8 +221,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): populate_context_dir(self.seed_dir, {"ETH0_IP": IP_BY_MACADDR}) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP_BY_MACADDR + "/" + IP4_PREFIX in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -265,8 +233,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP_BY_MACADDR + "/" + IP4_PREFIX in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -279,8 +247,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP_BY_MACADDR + "/" + IP4_PREFIX in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -296,8 +264,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP_BY_MACADDR + "/16" in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -313,8 +281,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP_BY_MACADDR + "/" + IP4_PREFIX in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -329,8 +297,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP6_GLOBAL + "/64" in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -345,8 +313,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP6_ULA + "/64" in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -362,8 +330,8 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP6_GLOBAL + "/" + IP6_PREFIX in results["network-interfaces"]["ethernets"][dev]["addresses"] ) @@ -379,13 +347,14 @@ def test_network_interfaces(self, m_get_phys_by_mac): ) results = ds.read_context_disk_dir(self.seed_dir, mock.Mock()) - self.assertTrue("network-interfaces" in results) - self.assertTrue( + assert "network-interfaces" in results + assert ( IP6_GLOBAL + "/64" in results["network-interfaces"]["ethernets"][dev]["addresses"] ) - def test_find_candidates(self): + @mock.patch(DS_PATH + ".util.find_devs_with") + def test_find_candidates(self, m_find_devs_with): def my_devs_with(criteria): return { "LABEL=CONTEXT": ["/dev/sdb"], @@ -393,18 +362,13 @@ def my_devs_with(criteria): "TYPE=iso9660": ["/dev/vdb"], }.get(criteria, []) - orig_find_devs_with = util.find_devs_with - try: - util.find_devs_with = my_devs_with - self.assertEqual( - ["/dev/sdb", "/dev/sr0", "/dev/vdb"], ds.find_candidate_devs() - ) - finally: - util.find_devs_with = orig_find_devs_with + m_find_devs_with.side_effect = my_devs_with + util.find_devs_with = my_devs_with + assert ["/dev/sdb", "/dev/sr0", "/dev/vdb"] == ds.find_candidate_devs() @mock.patch(DS_PATH + ".net.get_interfaces_by_mac", mock.Mock(return_value={})) -class TestOpenNebulaNetwork(unittest.TestCase): +class TestOpenNebulaNetwork: system_nics = ("eth0", "ens3") @@ -419,7 +383,7 @@ def test_context_devname(self): "02:00:0a:12:0f:0f": "ETH1", } net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(expected, net.context_devname) + assert expected == net.context_devname def test_get_nameservers(self): """ @@ -437,21 +401,21 @@ def test_get_nameservers(self): } net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_nameservers("eth0") - self.assertEqual(expected, val) + assert expected == val def test_get_mtu(self): """Verify get_mtu('device') correctly returns MTU size.""" context = {"ETH0_MTU": "1280"} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_mtu("eth0") - self.assertEqual("1280", val) + assert "1280" == val def test_get_ip(self): """Verify get_ip('device') correctly returns IPv4 address.""" context = {"ETH0_IP": PUBLIC_IP} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip("eth0", MACADDR) - self.assertEqual(PUBLIC_IP, val) + assert PUBLIC_IP == val def test_get_ip_emptystring(self): """ @@ -462,7 +426,7 @@ def test_get_ip_emptystring(self): context = {"ETH0_IP": ""} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip("eth0", MACADDR) - self.assertEqual(IP_BY_MACADDR, val) + assert IP_BY_MACADDR == val def test_get_ip6(self): """ @@ -476,7 +440,7 @@ def test_get_ip6(self): expected = [IP6_GLOBAL] net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip6("eth0") - self.assertEqual(expected, val) + assert expected == val def test_get_ip6_ula(self): """ @@ -490,7 +454,7 @@ def test_get_ip6_ula(self): expected = [IP6_ULA] net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip6("eth0") - self.assertEqual(expected, val) + assert expected == val def test_get_ip6_dual(self): """ @@ -504,7 +468,7 @@ def test_get_ip6_dual(self): expected = [IP6_GLOBAL, IP6_ULA] net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip6("eth0") - self.assertEqual(expected, val) + assert expected == val def test_get_ip6_prefix(self): """ @@ -513,7 +477,7 @@ def test_get_ip6_prefix(self): context = {"ETH0_IP6_PREFIX_LENGTH": IP6_PREFIX} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip6_prefix("eth0") - self.assertEqual(IP6_PREFIX, val) + assert IP6_PREFIX == val def test_get_ip6_prefix_emptystring(self): """ @@ -524,7 +488,7 @@ def test_get_ip6_prefix_emptystring(self): context = {"ETH0_IP6_PREFIX_LENGTH": ""} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_ip6_prefix("eth0") - self.assertEqual("64", val) + assert "64" == val def test_get_gateway(self): """ @@ -534,7 +498,7 @@ def test_get_gateway(self): context = {"ETH0_GATEWAY": "1.2.3.5"} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_gateway("eth0") - self.assertEqual("1.2.3.5", val) + assert "1.2.3.5" == val def test_get_gateway6(self): """ @@ -545,7 +509,7 @@ def test_get_gateway6(self): context = {"ETH0_" + k: IP6_GW} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_gateway6("eth0") - self.assertEqual(IP6_GW, val) + assert IP6_GW == val def test_get_mask(self): """ @@ -554,7 +518,7 @@ def test_get_mask(self): context = {"ETH0_MASK": "255.255.0.0"} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_mask("eth0") - self.assertEqual("255.255.0.0", val) + assert "255.255.0.0" == val def test_get_mask_emptystring(self): """ @@ -564,7 +528,7 @@ def test_get_mask_emptystring(self): context = {"ETH0_MASK": ""} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_mask("eth0") - self.assertEqual("255.255.255.0", val) + assert "255.255.255.0" == val def test_get_field(self): """ @@ -573,7 +537,7 @@ def test_get_field(self): context = {"ETH9_DUMMY": "DUMMY_VALUE"} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_field("eth9", "dummy") - self.assertEqual("DUMMY_VALUE", val) + assert "DUMMY_VALUE" == val def test_get_field_withdefaultvalue(self): """ @@ -583,7 +547,7 @@ def test_get_field_withdefaultvalue(self): context = {"ETH9_DUMMY": "DUMMY_VALUE"} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_field("eth9", "dummy", "DEFAULT_VALUE") - self.assertEqual("DUMMY_VALUE", val) + assert "DUMMY_VALUE" == val def test_get_field_withdefaultvalue_emptycontext(self): """ @@ -593,7 +557,7 @@ def test_get_field_withdefaultvalue_emptycontext(self): context = {"ETH9_DUMMY": ""} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_field("eth9", "dummy", "DEFAULT_VALUE") - self.assertEqual("DEFAULT_VALUE", val) + assert "DEFAULT_VALUE" == val def test_get_field_emptycontext(self): """ @@ -603,7 +567,7 @@ def test_get_field_emptycontext(self): context = {"ETH9_DUMMY": ""} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_field("eth9", "dummy") - self.assertEqual(None, val) + assert None is val def test_get_field_nonecontext(self): """ @@ -613,7 +577,7 @@ def test_get_field_nonecontext(self): context = {"ETH9_DUMMY": None} net = ds.OpenNebulaNetwork(context, mock.Mock()) val = net.get_field("eth9", "dummy") - self.assertEqual(None, val) + assert None is val @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_gen_conf_gateway(self, m_get_phys_by_mac): @@ -636,7 +600,7 @@ def test_gen_conf_gateway(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected # set ETH0_GATEWAY context = { @@ -656,7 +620,7 @@ def test_gen_conf_gateway(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_gen_conf_gateway6(self, m_get_phys_by_mac): @@ -679,7 +643,7 @@ def test_gen_conf_gateway6(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected # set ETH0_GATEWAY6 context = { @@ -699,7 +663,7 @@ def test_gen_conf_gateway6(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_gen_conf_ipv6address(self, m_get_phys_by_mac): @@ -724,7 +688,7 @@ def test_gen_conf_ipv6address(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected # set ETH0_IP6, ETH0_IP6_ULA, ETH0_IP6_PREFIX_LENGTH context = { @@ -749,7 +713,7 @@ def test_gen_conf_ipv6address(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_gen_conf_dns(self, m_get_phys_by_mac): @@ -774,7 +738,7 @@ def test_gen_conf_dns(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected # set DNS, ETH0_DNS, ETH0_SEARCH_DOMAIN context = { @@ -799,7 +763,7 @@ def test_gen_conf_dns(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_gen_conf_mtu(self, m_get_phys_by_mac): @@ -822,7 +786,7 @@ def test_gen_conf_mtu(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected # set ETH0_MTU context = { @@ -842,7 +806,7 @@ def test_gen_conf_mtu(self, m_get_phys_by_mac): } m_get_phys_by_mac.return_value = {MACADDR: nic} net = ds.OpenNebulaNetwork(context, mock.Mock()) - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_eth0(self, m_get_phys_by_mac): @@ -859,15 +823,14 @@ def test_eth0(self, m_get_phys_by_mac): }, } - self.assertEqual(net.gen_conf(), expected) + assert net.gen_conf() == expected @mock.patch(DS_PATH + ".get_physical_nics_by_mac") def test_distro_passed_through(self, m_get_physical_nics_by_mac): ds.OpenNebulaNetwork({}, mock.sentinel.distro) - self.assertEqual( - [mock.call(mock.sentinel.distro)], - m_get_physical_nics_by_mac.call_args_list, - ) + assert [ + mock.call(mock.sentinel.distro) + ] == m_get_physical_nics_by_mac.call_args_list def test_eth0_override(self): self.maxDiff = None @@ -904,7 +867,7 @@ def test_eth0_override(self): }, } - self.assertEqual(expected, net.gen_conf()) + assert expected == net.gen_conf() def test_eth0_v4v6_override(self): self.maxDiff = None @@ -949,7 +912,7 @@ def test_eth0_v4v6_override(self): }, } - self.assertEqual(expected, net.gen_conf()) + assert expected == net.gen_conf() def test_multiple_nics(self): """Test rendering multiple nics with names that differ from context.""" @@ -1019,7 +982,7 @@ def test_multiple_nics(self): }, } - self.assertEqual(expected, net.gen_conf()) + assert expected == net.gen_conf() class TestParseShellConfig: diff --git a/tests/unittests/sources/test_openstack.py b/tests/unittests/sources/test_openstack.py index 2d2e8a71157..f26c70f84fd 100644 --- a/tests/unittests/sources/test_openstack.py +++ b/tests/unittests/sources/test_openstack.py @@ -13,7 +13,7 @@ import pytest import responses -from cloudinit import helpers, settings, util +from cloudinit import settings, util from cloudinit.sources import UNSET, BrokenMetadata from cloudinit.sources import DataSourceOpenStack as ds from cloudinit.sources import convert_vendordata @@ -145,14 +145,17 @@ def _read_metadata_service(): return ds.read_metadata_service(BASE_URL, retries=0, timeout=0.1) -class TestOpenStackDataSource(test_helpers.CiTestCase): +class TestOpenStackDataSource: - with_logs = True VERSION = "latest" - def setUp(self): - super(TestOpenStackDataSource, self).setUp() - self.tmp = self.tmp_dir() + @pytest.fixture + def ds_os(self, paths): + return ds.DataSourceOpenStack( + settings.CFG_BUILTIN, + test_util.MockDistro(), + paths, + ) @responses.activate def test_successful(self): @@ -164,26 +167,22 @@ def test_successful(self): responses_mock=responses, ) f = _read_metadata_service() - self.assertEqual(VENDOR_DATA, f.get("vendordata")) - self.assertEqual(VENDOR_DATA2, f.get("vendordata2")) - self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) - self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) - self.assertEqual(2, len(f["files"])) - self.assertEqual(USER_DATA, f.get("userdata")) - self.assertEqual(EC2_META, f.get("ec2-metadata")) - self.assertEqual(2, f.get("version")) + assert VENDOR_DATA == f.get("vendordata") + assert VENDOR_DATA2 == f.get("vendordata2") + assert CONTENT_0 == f["files"]["/etc/foo.cfg"] + assert CONTENT_1 == f["files"]["/etc/bar/bar.cfg"] + assert 2 == len(f["files"]) + assert USER_DATA == f.get("userdata") + assert EC2_META == f.get("ec2-metadata") + assert 2 == f.get("version") metadata = f["metadata"] - self.assertEqual("nova", metadata.get("availability_zone")) - self.assertEqual("sm-foo-test.novalocal", metadata.get("hostname")) - self.assertEqual( - "sm-foo-test.novalocal", metadata.get("local-hostname") - ) - self.assertEqual("sm-foo-test", metadata.get("name")) - self.assertEqual( - "b0fa911b-69d4-4476-bbe2-1c92bff6535c", metadata.get("uuid") - ) - self.assertEqual( - "b0fa911b-69d4-4476-bbe2-1c92bff6535c", metadata.get("instance-id") + assert "nova" == metadata.get("availability_zone") + assert "sm-foo-test.novalocal" == metadata.get("hostname") + assert "sm-foo-test.novalocal" == metadata.get("local-hostname") + assert "sm-foo-test" == metadata.get("name") + assert "b0fa911b-69d4-4476-bbe2-1c92bff6535c" == metadata.get("uuid") + assert "b0fa911b-69d4-4476-bbe2-1c92bff6535c" == metadata.get( + "instance-id" ) @responses.activate @@ -192,13 +191,13 @@ def test_no_ec2(self): self.VERSION, {}, {}, OS_FILES, responses_mock=responses ) f = _read_metadata_service() - self.assertEqual(VENDOR_DATA, f.get("vendordata")) - self.assertEqual(VENDOR_DATA2, f.get("vendordata2")) - self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) - self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) - self.assertEqual(USER_DATA, f.get("userdata")) - self.assertEqual({}, f.get("ec2-metadata")) - self.assertEqual(2, f.get("version")) + assert VENDOR_DATA == f.get("vendordata") + assert VENDOR_DATA2 == f.get("vendordata2") + assert CONTENT_0 == f["files"]["/etc/foo.cfg"] + assert CONTENT_1 == f["files"]["/etc/bar/bar.cfg"] + assert USER_DATA == f.get("userdata") + assert {} == f.get("ec2-metadata") + assert 2 == f.get("version") @responses.activate def test_bad_metadata(self): @@ -209,7 +208,8 @@ def test_bad_metadata(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - self.assertRaises(openstack.NonReadable, _read_metadata_service) + with pytest.raises(openstack.NonReadable): + _read_metadata_service() @responses.activate def test_bad_uuid(self): @@ -222,7 +222,8 @@ def test_bad_uuid(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - self.assertRaises(BrokenMetadata, _read_metadata_service) + with pytest.raises(BrokenMetadata): + _read_metadata_service() @responses.activate def test_userdata_empty(self): @@ -234,11 +235,11 @@ def test_userdata_empty(self): self.VERSION, {}, {}, os_files, responses_mock=responses ) f = _read_metadata_service() - self.assertEqual(VENDOR_DATA, f.get("vendordata")) - self.assertEqual(VENDOR_DATA2, f.get("vendordata2")) - self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) - self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) - self.assertFalse(f.get("userdata")) + assert VENDOR_DATA == f.get("vendordata") + assert VENDOR_DATA2 == f.get("vendordata2") + assert CONTENT_0 == f["files"]["/etc/foo.cfg"] + assert CONTENT_1 == f["files"]["/etc/bar/bar.cfg"] + assert not f.get("userdata") @responses.activate def test_vendordata_empty(self): @@ -250,9 +251,9 @@ def test_vendordata_empty(self): self.VERSION, {}, {}, os_files, responses_mock=responses ) f = _read_metadata_service() - self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) - self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) - self.assertFalse(f.get("vendordata")) + assert CONTENT_0 == f["files"]["/etc/foo.cfg"] + assert CONTENT_1 == f["files"]["/etc/bar/bar.cfg"] + assert not f.get("vendordata") @responses.activate def test_vendordata2_empty(self): @@ -264,9 +265,9 @@ def test_vendordata2_empty(self): self.VERSION, {}, {}, os_files, responses_mock=responses ) f = _read_metadata_service() - self.assertEqual(CONTENT_0, f["files"]["/etc/foo.cfg"]) - self.assertEqual(CONTENT_1, f["files"]["/etc/bar/bar.cfg"]) - self.assertFalse(f.get("vendordata2")) + assert CONTENT_0 == f["files"]["/etc/foo.cfg"] + assert CONTENT_1 == f["files"]["/etc/bar/bar.cfg"] + assert not f.get("vendordata2") @responses.activate def test_vendordata_invalid(self): @@ -277,7 +278,8 @@ def test_vendordata_invalid(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - self.assertRaises(BrokenMetadata, _read_metadata_service) + with pytest.raises(BrokenMetadata): + _read_metadata_service() @responses.activate def test_vendordata2_invalid(self): @@ -288,7 +290,8 @@ def test_vendordata2_invalid(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - self.assertRaises(BrokenMetadata, _read_metadata_service) + with pytest.raises(BrokenMetadata): + _read_metadata_service() @responses.activate def test_metadata_invalid(self): @@ -299,11 +302,12 @@ def test_metadata_invalid(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - self.assertRaises(BrokenMetadata, _read_metadata_service) + with pytest.raises(BrokenMetadata): + _read_metadata_service() @responses.activate @test_helpers.mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") - def test_datasource(self, m_dhcp): + def test_datasource(self, m_dhcp, ds_os): _register_uris( self.VERSION, EC2_FILES, @@ -311,23 +315,18 @@ def test_datasource(self, m_dhcp): OS_FILES, responses_mock=responses, ) - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, - test_util.MockDistro(), - helpers.Paths({"run_dir": self.tmp}), - ) - self.assertIsNone(ds_os.version) + assert ds_os.version is None with mock.patch.object(ds_os, "override_ds_detect", return_value=True): - self.assertTrue(ds_os.get_data()) - self.assertEqual(2, ds_os.version) + assert ds_os.get_data() + assert 2 == ds_os.version md = dict(ds_os.metadata) md.pop("instance-id", None) md.pop("local-hostname", None) - self.assertEqual(OSTACK_META, md) - self.assertEqual(EC2_META, ds_os.ec2_metadata) - self.assertEqual(USER_DATA, ds_os.userdata_raw) - self.assertEqual(2, len(ds_os.files)) - self.assertIsNone(ds_os.vendordata_raw) + assert OSTACK_META == md + assert EC2_META == ds_os.ec2_metadata + assert USER_DATA == ds_os.userdata_raw + assert 2 == len(ds_os.files) + assert ds_os.vendordata_raw is None m_dhcp.assert_not_called() @responses.activate @@ -336,7 +335,7 @@ def test_datasource(self, m_dhcp): "cloudinit.net.ephemeral.maybe_perform_dhcp_discovery" ) @pytest.mark.usefixtures("disable_netdev_info") - def test_local_datasource(self, m_dhcp, m_net): + def test_local_datasource(self, m_dhcp, m_net, paths, tmp_path): """OpenStackLocal calls EphemeralDHCPNetwork and gets instance data.""" _register_uris( self.VERSION, @@ -346,9 +345,9 @@ def test_local_datasource(self, m_dhcp, m_net): responses_mock=responses, ) distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir + distro.get_tmp_exec_path = str(tmp_path) ds_os_local = ds.DataSourceOpenStackLocal( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) + settings.CFG_BUILTIN, distro, paths ) distro.fallback_interface = "eth9" # Monkey patch for dhcp m_dhcp.return_value = { @@ -359,26 +358,25 @@ def test_local_datasource(self, m_dhcp, m_net): "broadcast-address": "192.168.2.255", } - self.assertIsNone(ds_os_local.version) + assert ds_os_local.version is None with test_helpers.mock.patch.object( ds_os_local, "override_ds_detect" ) as m_detect_os: m_detect_os.return_value = True - found = ds_os_local.get_data() - self.assertTrue(found) - self.assertEqual(2, ds_os_local.version) + assert ds_os_local.get_data() is True + assert 2 == ds_os_local.version md = dict(ds_os_local.metadata) md.pop("instance-id", None) md.pop("local-hostname", None) - self.assertEqual(OSTACK_META, md) - self.assertEqual(EC2_META, ds_os_local.ec2_metadata) - self.assertEqual(USER_DATA, ds_os_local.userdata_raw) - self.assertEqual(2, len(ds_os_local.files)) - self.assertIsNone(ds_os_local.vendordata_raw) + assert OSTACK_META == md + assert EC2_META == ds_os_local.ec2_metadata + assert USER_DATA == ds_os_local.userdata_raw + assert 2 == len(ds_os_local.files) + assert ds_os_local.vendordata_raw is None m_dhcp.assert_called_with(distro, "eth9", None) @responses.activate - def test_bad_datasource_meta(self): + def test_bad_datasource_meta(self, caplog, ds_os): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("meta_data.json"): @@ -386,27 +384,22 @@ def test_bad_datasource_meta(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, - test_util.MockDistro(), - helpers.Paths({"run_dir": self.tmp}), - ) - self.assertIsNone(ds_os.version) + assert ds_os.version is None with test_helpers.mock.patch.object( ds_os, "override_ds_detect" ) as m_detect_os: m_detect_os.return_value = True found = ds_os.get_data() - self.assertFalse(found) - self.assertIsNone(ds_os.version) - self.assertRegex( - self.logs.getvalue(), + assert not found + assert ds_os.version is None + assert re.search( r"InvalidMetaDataException: Broken metadata address" r" http://(169.254.169.254|\[fe80::a9fe:a9fe\])", + caplog.text, ) @responses.activate - def test_no_datasource(self): + def test_no_datasource(self, ds_os): os_files = copy.deepcopy(OS_FILES) for k in list(os_files.keys()): if k.endswith("meta_data.json"): @@ -414,26 +407,18 @@ def test_no_datasource(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, - test_util.MockDistro(), - helpers.Paths({"run_dir": self.tmp}), - ) ds_os.ds_cfg = { "max_wait": 0, "timeout": 0, } - self.assertIsNone(ds_os.version) + assert ds_os.version is None with mock.patch.object(ds_os, "override_ds_detect", return_value=True): - self.assertFalse(ds_os.get_data()) - self.assertIsNone(ds_os.version) + assert not ds_os.get_data() + assert ds_os.version is None - def test_network_config_disabled_by_datasource_config(self): + def test_network_config_disabled_by_datasource_config(self, ds_os): """The network_config can be disabled from datasource config.""" mock_path = MOCK_PATH + "openstack.convert_net_json" - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": self.tmp}) - ) ds_os.ds_cfg = {"apply_network_config": False} sample_json = { "links": [{"ethernet_mac_address": "mymac"}], @@ -442,16 +427,13 @@ def test_network_config_disabled_by_datasource_config(self): } ds_os.network_json = sample_json # Ignore this content from metadata with test_helpers.mock.patch(mock_path) as m_convert_json: - self.assertIsNone(ds_os.network_config) + assert ds_os.network_config is None m_convert_json.assert_not_called() - def test_network_config_from_network_json(self): + def test_network_config_from_network_json(self, caplog, ds_os): """The datasource gets network_config from network_data.json.""" mock_path = MOCK_PATH + "openstack.convert_net_json" example_cfg = {"version": 1, "config": []} - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": self.tmp}) - ) sample_json = { "links": [{"ethernet_mac_address": "mymac"}], "networks": [], @@ -460,26 +442,21 @@ def test_network_config_from_network_json(self): ds_os.network_json = sample_json with test_helpers.mock.patch(mock_path) as m_convert_json: m_convert_json.return_value = example_cfg - self.assertEqual(example_cfg, ds_os.network_config) - self.assertIn( - "network config provided via network_json", self.logs.getvalue() - ) + assert example_cfg == ds_os.network_config + assert "network config provided via network_json" in caplog.text m_convert_json.assert_called_with(sample_json, known_macs=None) - def test_network_config_cached(self): + def test_network_config_cached(self, ds_os): """The datasource caches the network_config property.""" mock_path = MOCK_PATH + "openstack.convert_net_json" example_cfg = {"version": 1, "config": []} - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": self.tmp}) - ) ds_os._network_config = example_cfg with test_helpers.mock.patch(mock_path) as m_convert_json: - self.assertEqual(example_cfg, ds_os.network_config) + assert example_cfg == ds_os.network_config m_convert_json.assert_not_called() @responses.activate - def test_disabled_datasource(self): + def test_disabled_datasource(self, ds_os): os_files = copy.deepcopy(OS_FILES) os_meta = copy.deepcopy(OSTACK_META) os_meta["meta"] = { @@ -491,26 +468,21 @@ def test_disabled_datasource(self): _register_uris( self.VERSION, {}, {}, os_files, responses_mock=responses ) - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, - test_util.MockDistro(), - helpers.Paths({"run_dir": self.tmp}), - ) ds_os.ds_cfg = { "max_wait": 0, "timeout": 0, } - self.assertIsNone(ds_os.version) + assert ds_os.version is None with test_helpers.mock.patch.object( ds_os, "override_ds_detect" ) as m_detect_os: m_detect_os.return_value = True found = ds_os.get_data() - self.assertFalse(found) - self.assertIsNone(ds_os.version) + assert not found + assert ds_os.version is None @responses.activate - def test_wb__crawl_metadata_does_not_persist(self): + def test_wb__crawl_metadata_does_not_persist(self, ds_os): """_crawl_metadata returns current metadata and does not cache.""" _register_uris( self.VERSION, @@ -519,45 +491,37 @@ def test_wb__crawl_metadata_does_not_persist(self): OS_FILES, responses_mock=responses, ) - ds_os = ds.DataSourceOpenStack( - settings.CFG_BUILTIN, - test_util.MockDistro(), - helpers.Paths({"run_dir": self.tmp}), - ) crawled_data = ds_os._crawl_metadata() - self.assertEqual(UNSET, ds_os.ec2_metadata) - self.assertIsNone(ds_os.userdata_raw) - self.assertEqual(0, len(ds_os.files)) - self.assertIsNone(ds_os.vendordata_raw) - self.assertEqual( - [ - "dsmode", - "ec2-metadata", - "files", - "metadata", - "networkdata", - "userdata", - "vendordata", - "vendordata2", - "version", - ], - sorted(crawled_data.keys()), - ) - self.assertEqual("local", crawled_data["dsmode"]) - self.assertEqual(EC2_META, crawled_data["ec2-metadata"]) - self.assertEqual(2, len(crawled_data["files"])) + assert UNSET == ds_os.ec2_metadata + assert ds_os.userdata_raw is None + assert 0 == len(ds_os.files) + assert ds_os.vendordata_raw is None + assert [ + "dsmode", + "ec2-metadata", + "files", + "metadata", + "networkdata", + "userdata", + "vendordata", + "vendordata2", + "version", + ] == sorted(crawled_data.keys()) + assert "local" == crawled_data["dsmode"] + assert EC2_META == crawled_data["ec2-metadata"] + assert 2 == len(crawled_data["files"]) md = copy.deepcopy(crawled_data["metadata"]) md.pop("instance-id") md.pop("local-hostname") - self.assertEqual(OSTACK_META, md) - self.assertEqual( - json.loads(OS_FILES["openstack/latest/network_data.json"]), - crawled_data["networkdata"], + assert OSTACK_META == md + assert ( + json.loads(OS_FILES["openstack/latest/network_data.json"]) + == crawled_data["networkdata"] ) - self.assertEqual(USER_DATA, crawled_data["userdata"]) - self.assertEqual(VENDOR_DATA, crawled_data["vendordata"]) - self.assertEqual(VENDOR_DATA2, crawled_data["vendordata2"]) - self.assertEqual(2, crawled_data["version"]) + assert USER_DATA == crawled_data["userdata"] + assert VENDOR_DATA == crawled_data["vendordata"] + assert VENDOR_DATA2 == crawled_data["vendordata2"] + assert 2 == crawled_data["version"] class TestVendorDataLoading(test_helpers.TestCase): @@ -566,55 +530,54 @@ def cvj(self, data): def test_vd_load_none(self): # non-existant vendor-data should return none - self.assertIsNone(self.cvj(None)) + assert self.cvj(None) is None def test_vd_load_string(self): - self.assertEqual(self.cvj("foobar"), "foobar") + assert self.cvj("foobar") == "foobar" def test_vd_load_list(self): data = [{"foo": "bar"}, "mystring", list(["another", "list"])] - self.assertEqual(self.cvj(data), data) + assert self.cvj(data) == data def test_vd_load_dict_no_ci(self): - self.assertIsNone(self.cvj({"foo": "bar"})) + assert self.cvj({"foo": "bar"}) is None def test_vd_load_dict_ci_dict(self): - self.assertRaises( - ValueError, self.cvj, {"foo": "bar", "cloud-init": {"x": 1}} - ) + with pytest.raises(ValueError): + self.cvj({"foo": "bar", "cloud-init": {"x": 1}}) def test_vd_load_dict_ci_string(self): data = {"foo": "bar", "cloud-init": "VENDOR_DATA"} - self.assertEqual(self.cvj(data), data["cloud-init"]) + assert self.cvj(data) == data["cloud-init"] def test_vd_load_dict_ci_list(self): data = {"foo": "bar", "cloud-init": ["VD_1", "VD_2"]} - self.assertEqual(self.cvj(data), data["cloud-init"]) + assert self.cvj(data) == data["cloud-init"] @test_helpers.mock.patch(MOCK_PATH + "util.is_x86") -class TestDetectOpenStack(test_helpers.CiTestCase): +class TestDetectOpenStack: def setUp(self): self.tmp = self.tmp_dir() - def _fake_ds(self) -> ds.DataSourceOpenStack: + @pytest.fixture + def fake_ds(self, paths) -> ds.DataSourceOpenStack: return ds.DataSourceOpenStack( settings.CFG_BUILTIN, test_util.MockDistro(), - helpers.Paths({"run_dir": self.tmp}), + paths, ) - def test_ds_detect_non_intel_x86(self, m_is_x86): + def test_ds_detect_non_intel_x86(self, m_is_x86, fake_ds): """Return True on non-intel platforms because dmi isn't conclusive.""" m_is_x86.return_value = False - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True", - ) + assert fake_ds.ds_detect(), "Expected ds_detect == True" @test_helpers.mock.patch(MOCK_PATH + "util.get_proc_env") @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_not_ds_detect_intel_x86_ec2(self, m_dmi, m_proc_env, m_is_x86): + def test_not_ds_detect_intel_x86_ec2( + self, m_dmi, m_proc_env, m_is_x86, fake_ds + ): """Return False on EC2 platforms.""" m_is_x86.return_value = True # No product_name in proc/1/environ @@ -628,28 +591,24 @@ def fake_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_dmi_read - self.assertFalse( - self._fake_ds().ds_detect(), - "Expected ds_detect == False on EC2", - ) + assert not fake_ds.ds_detect(), "Expected ds_detect == False on EC2" m_proc_env.assert_called_with(1) @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_intel_product_name_compute(self, m_dmi, m_is_x86): + def test_ds_detect_intel_product_name_compute( + self, m_dmi, m_is_x86, fake_ds + ): """Return True on OpenStack compute and nova instances.""" m_is_x86.return_value = True openstack_product_names = ["OpenStack Nova", "OpenStack Compute"] for product_name in openstack_product_names: m_dmi.return_value = product_name - self.assertTrue( - self._fake_ds().ds_detect(), - "Failed to ds_detect", - ) + assert fake_ds.ds_detect(), "Failed to ds_detect" @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") def test_ds_detect_opentelekomcloud_chassis_asset_tag( - self, m_dmi, m_is_x86 + self, m_dmi, m_is_x86, fake_ds ): """Return True on OpenStack reporting OpenTelekomCloud asset-tag.""" m_is_x86.return_value = True @@ -662,13 +621,14 @@ def fake_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_dmi_read - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True on OpenTelekomCloud", - ) + assert ( + fake_ds.ds_detect() + ), "Expected ds_detect == True on OpenTelekomCloud" @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_sapccloud_chassis_asset_tag(self, m_dmi, m_is_x86): + def test_ds_detect_sapccloud_chassis_asset_tag( + self, m_dmi, m_is_x86, fake_ds + ): """Return True on OpenStack reporting SAP CCloud VM asset-tag.""" m_is_x86.return_value = True @@ -680,13 +640,14 @@ def fake_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_dmi_read - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True on SAP CCloud VM", - ) + assert ( + fake_ds.ds_detect() + ), "Expected ds_detect == True on SAP CCloud VM" @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_huaweicloud_chassis_asset_tag(self, m_dmi, m_is_x86): + def test_ds_detect_huaweicloud_chassis_asset_tag( + self, m_dmi, m_is_x86, fake_ds + ): """Return True on OpenStack reporting Huawei Cloud VM asset-tag.""" m_is_x86.return_value = True @@ -698,14 +659,13 @@ def fake_asset_tag_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_asset_tag_dmi_read - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True on Huawei Cloud VM", - ) + assert ( + fake_ds.ds_detect() + ), "Expected ds_detect == True on Huawei Cloud VM" @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") def test_ds_detect_samsung_cloud_platform_chassis_asset_tag( - self, m_dmi, m_is_x86 + self, m_dmi, m_is_x86, fake_ds ): """Return True on OpenStack reporting Samsung Cloud Platform VM asset-tag.""" @@ -719,13 +679,14 @@ def fake_asset_tag_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_asset_tag_dmi_read - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True on Samsung Cloud Platform VM", - ) + assert ( + fake_ds.ds_detect() + ), "Expected ds_detect == True on Samsung Cloud Platform VM" @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_oraclecloud_chassis_asset_tag(self, m_dmi, m_is_x86): + def test_ds_detect_oraclecloud_chassis_asset_tag( + self, m_dmi, m_is_x86, fake_ds + ): """Return True on OpenStack reporting Oracle cloud asset-tag.""" m_is_x86.return_value = True @@ -737,20 +698,18 @@ def fake_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_dmi_read - ds = self._fake_ds() + ds = fake_ds ds.sys_cfg = {"datasource_list": ["Oracle"]} - self.assertTrue( - ds.ds_detect(), - "Expected ds_detect == True on OracleCloud.com", - ) + assert ds.ds_detect(), "Expected ds_detect == True on OracleCloud.com" ds.sys_cfg = {"datasource_list": []} - self.assertFalse( - ds.ds_detect(), - "Expected ds_detect == False.", - ) + assert not ds.ds_detect(), "Expected ds_detect == False." - def _test_ds_detect_nova_compute_chassis_asset_tag( - self, m_dmi, m_is_x86, chassis_tag + @pytest.mark.parametrize( + ["chassis_tag"], [("OpenStack Nova",), ("OpenStack Compute",)] + ) + @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") + def test_ds_detect_chassis_asset_tag( + self, m_dmi, m_is_x86, chassis_tag, fake_ds ): """Return True on OpenStack reporting generic asset-tag.""" m_is_x86.return_value = True @@ -763,26 +722,15 @@ def fake_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_dmi_read - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True on Generic OpenStack Platform", - ) - - @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_nova_chassis_asset_tag(self, m_dmi, m_is_x86): - self._test_ds_detect_nova_compute_chassis_asset_tag( - m_dmi, m_is_x86, "OpenStack Nova" - ) - - @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_compute_chassis_asset_tag(self, m_dmi, m_is_x86): - self._test_ds_detect_nova_compute_chassis_asset_tag( - m_dmi, m_is_x86, "OpenStack Compute" - ) + assert ( + fake_ds.ds_detect() + ), "Expected ds_detect == True on Generic OpenStack Platform" @test_helpers.mock.patch(MOCK_PATH + "util.get_proc_env") @test_helpers.mock.patch(MOCK_PATH + "dmi.read_dmi_data") - def test_ds_detect_by_proc_1_environ(self, m_dmi, m_proc_env, m_is_x86): + def test_ds_detect_by_proc_1_environ( + self, m_dmi, m_proc_env, m_is_x86, fake_ds + ): """Return True when nova product_name specified in /proc/1/environ.""" m_is_x86.return_value = True # Nova product_name in proc/1/environ @@ -799,14 +747,13 @@ def fake_dmi_read(dmi_key): assert False, "Unexpected dmi read of %s" % dmi_key m_dmi.side_effect = fake_dmi_read - self.assertTrue( - self._fake_ds().ds_detect(), - "Expected ds_detect == True on OpenTelekomCloud", - ) + assert ( + fake_ds.ds_detect() + ), "Expected ds_detect == True on OpenTelekomCloud" m_proc_env.assert_called_with(1) -class TestMetadataReader(test_helpers.CiTestCase): +class TestMetadataReader: """Test the MetadataReader.""" burl = "http://169.254.169.254/" @@ -856,9 +803,9 @@ def test__find_working_version(self): openstack.OS_LATEST, ] ) - self.assertEqual( - openstack.OS_LIBERTY, - openstack.MetadataReader(self.burl)._find_working_version(), + assert ( + openstack.OS_LIBERTY + == openstack.MetadataReader(self.burl)._find_working_version() ) @responses.activate @@ -866,9 +813,9 @@ def test__find_working_version_uses_latest(self): """'latest' should be used if no supported versions.""" unsup1, unsup2 = ("2016-11-09", "2017-06-06") self.register_versions([unsup1, unsup2, openstack.OS_LATEST]) - self.assertEqual( - openstack.OS_LATEST, - openstack.MetadataReader(self.burl)._find_working_version(), + assert ( + openstack.OS_LATEST + == openstack.MetadataReader(self.burl)._find_working_version() ) @responses.activate @@ -909,5 +856,5 @@ def test_read_v2_os_ocata(self): } reader = openstack.MetadataReader(self.burl) reader._read_ec2_metadata = mock_read_ec2 - self.assertEqual(expected, reader.read_v2()) - self.assertEqual(1, mock_read_ec2.call_count) + assert expected == reader.read_v2() + assert 1 == mock_read_ec2.call_count diff --git a/tests/unittests/sources/test_ovf.py b/tests/unittests/sources/test_ovf.py index b3bc84a8578..c5392f9ae76 100644 --- a/tests/unittests/sources/test_ovf.py +++ b/tests/unittests/sources/test_ovf.py @@ -5,13 +5,15 @@ # This file is part of cloud-init. See LICENSE file for license information. import base64 +import os from collections import OrderedDict from textwrap import dedent +import pytest + from cloudinit import subp, util -from cloudinit.helpers import Paths from cloudinit.sources import DataSourceOVF as dsovf -from tests.unittests.helpers import CiTestCase, mock +from tests.unittests.helpers import mock MPATH = "cloudinit.sources.DataSourceOVF." @@ -48,7 +50,7 @@ def fill_properties(props, template=OVF_ENV_CONTENT): return template.format(properties=properties) -class TestReadOvfEnv(CiTestCase): +class TestReadOvfEnv: def test_with_b64_userdata(self): user_data = "#!/bin/sh\necho hello world\n" user_data_b64 = base64.b64encode(user_data.encode()).decode() @@ -59,26 +61,26 @@ def test_with_b64_userdata(self): } env = fill_properties(props) md, ud, cfg = dsovf.read_ovf_environment(env) - self.assertEqual({"instance-id": "inst-001"}, md) - self.assertEqual(user_data.encode(), ud) - self.assertEqual({"password": "passw0rd"}, cfg) + assert {"instance-id": "inst-001"} == md + assert user_data.encode() == ud + assert {"password": "passw0rd"} == cfg def test_with_non_b64_userdata(self): user_data = "my-user-data" props = {"user-data": user_data, "instance-id": "inst-001"} env = fill_properties(props) md, ud, cfg = dsovf.read_ovf_environment(env) - self.assertEqual({"instance-id": "inst-001"}, md) - self.assertEqual(user_data.encode(), ud) - self.assertEqual({}, cfg) + assert {"instance-id": "inst-001"} == md + assert user_data.encode() == ud + assert {} == cfg def test_with_no_userdata(self): props = {"password": "passw0rd", "instance-id": "inst-001"} env = fill_properties(props) md, ud, cfg = dsovf.read_ovf_environment(env) - self.assertEqual({"instance-id": "inst-001"}, md) - self.assertEqual({"password": "passw0rd"}, cfg) - self.assertIsNone(ud) + assert {"instance-id": "inst-001"} == md + assert {"password": "passw0rd"} == cfg + assert ud is None def test_with_b64_network_config_enable_read_network(self): network_config = dedent( @@ -109,27 +111,24 @@ def test_with_b64_network_config_enable_read_network(self): } env = fill_properties(props) md, ud, cfg = dsovf.read_ovf_environment(env, True) - self.assertEqual("inst-001", md["instance-id"]) - self.assertEqual({"password": "passw0rd"}, cfg) - self.assertEqual( - { - "version": 2, - "ethernets": { - "nics": { - "nameservers": { - "addresses": ["127.0.0.53"], - "search": ["eng.vmware.com", "vmware.com"], - }, - "match": {"name": "eth*"}, - "gateway4": "10.10.10.253", - "dhcp4": False, - "addresses": ["10.10.10.1/24"], - } - }, + assert "inst-001" == md["instance-id"] + assert {"password": "passw0rd"} == cfg + assert { + "version": 2, + "ethernets": { + "nics": { + "nameservers": { + "addresses": ["127.0.0.53"], + "search": ["eng.vmware.com", "vmware.com"], + }, + "match": {"name": "eth*"}, + "gateway4": "10.10.10.253", + "dhcp4": False, + "addresses": ["10.10.10.1/24"], + } }, - md["network-config"], - ) - self.assertIsNone(ud) + } == md["network-config"] + assert ud is None def test_with_non_b64_network_config_enable_read_network(self): network_config = dedent( @@ -159,9 +158,9 @@ def test_with_non_b64_network_config_enable_read_network(self): } env = fill_properties(props) md, ud, cfg = dsovf.read_ovf_environment(env, True) - self.assertEqual({"instance-id": "inst-001"}, md) - self.assertEqual({"password": "passw0rd"}, cfg) - self.assertIsNone(ud) + assert {"instance-id": "inst-001"} == md + assert {"password": "passw0rd"} == cfg + assert ud is None def test_with_b64_network_config_disable_read_network(self): network_config = dedent( @@ -192,52 +191,45 @@ def test_with_b64_network_config_disable_read_network(self): } env = fill_properties(props) md, ud, cfg = dsovf.read_ovf_environment(env) - self.assertEqual({"instance-id": "inst-001"}, md) - self.assertEqual({"password": "passw0rd"}, cfg) - self.assertIsNone(ud) + assert {"instance-id": "inst-001"} == md + assert {"password": "passw0rd"} == cfg + assert ud is None -class TestDatasourceOVF(CiTestCase): - with_logs = True +class TestDatasourceOVF: + @pytest.fixture + def ds(self, paths): + return dsovf.DataSourceOVF(sys_cfg={}, distro={}, paths=paths) - def setUp(self): - super(TestDatasourceOVF, self).setUp() - self.datasource = dsovf.DataSourceOVF - self.tdir = self.tmp_dir() - - def test_get_data_seed_dir(self): + def test_get_data_seed_dir(self, ds, paths): """Platform info properly reports when getting data from seed dir.""" - paths = Paths({"cloud_dir": self.tdir, "run_dir": self.tdir}) # Write ovf-env.xml seed file - seed_dir = self.tmp_path("seed", dir=self.tdir) - ovf_env = self.tmp_path("ovf-env.xml", dir=seed_dir) + ovf_env = os.path.join(paths.seed_dir, "ovf-env.xml") util.write_file(ovf_env, OVF_ENV_CONTENT) - ds = self.datasource(sys_cfg={}, distro={}, paths=paths) - self.assertEqual("ovf", ds.cloud_name) - self.assertEqual("ovf", ds.platform_type) + assert "ovf" == ds.cloud_name + assert "ovf" == ds.platform_type with mock.patch(MPATH + "transport_vmware_guestinfo") as m_guestd: with mock.patch(MPATH + "transport_iso9660") as m_iso9660: m_iso9660.return_value = NOT_FOUND m_guestd.return_value = NOT_FOUND - self.assertTrue(ds.get_data()) - self.assertEqual( - "ovf (%s/seed/ovf-env.xml)" % self.tdir, ds.subplatform + assert ds.get_data() + assert ( + "ovf (%s/ovf-env.xml)" % paths.seed_dir == ds.subplatform ) + @pytest.mark.parametrize( + ["guestinfo", "iso"], + [ + pytest.param(False, True, id="vmware_guestinfo"), + pytest.param(True, False, id="iso9660"), + ], + ) @mock.patch("cloudinit.subp.subp") @mock.patch("cloudinit.sources.DataSource.persist_instance_data") def test_get_data_vmware_guestinfo_with_network_config( - self, m_persist, m_subp + self, m_persist, m_subp, guestinfo, iso, ds, paths ): - self._test_get_data_with_network_config(guestinfo=False, iso=True) - - @mock.patch("cloudinit.subp.subp") - @mock.patch("cloudinit.sources.DataSource.persist_instance_data") - def test_get_data_iso9660_with_network_config(self, m_persist, m_subp): - self._test_get_data_with_network_config(guestinfo=True, iso=False) - - def _test_get_data_with_network_config(self, guestinfo, iso): network_config = dedent( """\ network: @@ -264,8 +256,6 @@ def _test_get_data_with_network_config(self, guestinfo, iso): "instance-id": "inst-001", } env = fill_properties(props) - paths = Paths({"cloud_dir": self.tdir, "run_dir": self.tdir}) - ds = self.datasource(sys_cfg={}, distro={}, paths=paths) with mock.patch( MPATH + "transport_vmware_guestinfo", return_value=env if guestinfo else NOT_FOUND, @@ -274,36 +264,33 @@ def _test_get_data_with_network_config(self, guestinfo, iso): MPATH + "transport_iso9660", return_value=env if iso else NOT_FOUND, ): - self.assertTrue(ds.get_data()) - self.assertEqual("inst-001", ds.metadata["instance-id"]) - self.assertEqual( - { - "version": 2, - "ethernets": { - "nics": { - "nameservers": { - "addresses": ["127.0.0.53"], - "search": ["vmware.com"], - }, - "match": {"name": "eth*"}, - "gateway4": "10.10.10.253", - "dhcp4": False, - "addresses": ["10.10.10.1/24"], - } - }, + assert ds.get_data() is True + assert "inst-001" == ds.metadata["instance-id"] + assert { + "version": 2, + "ethernets": { + "nics": { + "nameservers": { + "addresses": ["127.0.0.53"], + "search": ["vmware.com"], + }, + "match": {"name": "eth*"}, + "gateway4": "10.10.10.253", + "dhcp4": False, + "addresses": ["10.10.10.1/24"], + } }, - ds.network_config, - ) + } == ds.network_config -class TestTransportIso9660(CiTestCase): - def setUp(self): - super(TestTransportIso9660, self).setUp() - self.add_patch("cloudinit.util.find_devs_with", "m_find_devs_with") - self.add_patch("cloudinit.util.mounts", "m_mounts") - self.add_patch("cloudinit.util.mount_cb", "m_mount_cb") - self.add_patch( - "cloudinit.sources.DataSourceOVF.get_ovf_env", "m_get_ovf_env" +class TestTransportIso9660: + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + self.m_find_devs_with = mocker.patch("cloudinit.util.find_devs_with") + self.m_mounts = mocker.patch("cloudinit.util.mounts") + self.m_mount_cb = mocker.patch("cloudinit.util.mount_cb") + self.m_get_ovf_env = mocker.patch( + "cloudinit.sources.DataSourceOVF.get_ovf_env" ) self.m_get_ovf_env.return_value = ("myfile", "mycontent") @@ -318,7 +305,7 @@ def test_find_already_mounted(self): } self.m_mounts.return_value = mounts - self.assertEqual("mycontent", dsovf.transport_iso9660()) + assert "mycontent" == dsovf.transport_iso9660() def test_find_already_mounted_skips_non_iso9660(self): """Check we call get_ovf_env ignoring non iso9660""" @@ -342,7 +329,7 @@ def test_find_already_mounted_skips_non_iso9660(self): sorted(mounts.items(), key=lambda t: t[0]) ) - self.assertEqual("mycontent", dsovf.transport_iso9660()) + assert "mycontent" == dsovf.transport_iso9660() def test_find_already_mounted_matches_kname(self): """Check we dont regex match on basename of the device""" @@ -356,7 +343,7 @@ def test_find_already_mounted_matches_kname(self): # we're skipping an entry which fails to match. self.m_mounts.return_value = mounts - self.assertEqual(NOT_FOUND, dsovf.transport_iso9660()) + assert NOT_FOUND == dsovf.transport_iso9660() def test_mount_cb_called_on_blkdevs_with_iso9660(self): """Check we call mount_cb on blockdevs with iso9660 only""" @@ -364,7 +351,7 @@ def test_mount_cb_called_on_blkdevs_with_iso9660(self): self.m_find_devs_with.return_value = ["/dev/sr0"] self.m_mount_cb.return_value = ("myfile", "mycontent") - self.assertEqual("mycontent", dsovf.transport_iso9660()) + assert "mycontent" == dsovf.transport_iso9660() self.m_mount_cb.assert_called_with( "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660" ) @@ -379,7 +366,7 @@ def test_mount_cb_called_on_blkdevs_with_iso9660_check_regex(self): ] self.m_mount_cb.return_value = ("myfile", "mycontent") - self.assertEqual("mycontent", dsovf.transport_iso9660()) + assert "mycontent" == dsovf.transport_iso9660() self.m_mount_cb.assert_called_with( "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660" ) @@ -389,8 +376,8 @@ def test_mount_cb_not_called_no_matches(self): self.m_mounts.return_value = {} self.m_find_devs_with.return_value = ["/dev/vg/myovf"] - self.assertEqual(NOT_FOUND, dsovf.transport_iso9660()) - self.assertEqual(0, self.m_mount_cb.call_count) + assert NOT_FOUND == dsovf.transport_iso9660() + assert 0 == self.m_mount_cb.call_count def test_mount_cb_called_require_iso_false(self): """Check we call mount_cb on blockdevs with require_iso=False""" @@ -398,9 +385,7 @@ def test_mount_cb_called_require_iso_false(self): self.m_find_devs_with.return_value = ["/dev/xvdz"] self.m_mount_cb.return_value = ("myfile", "mycontent") - self.assertEqual( - "mycontent", dsovf.transport_iso9660(require_iso=False) - ) + assert "mycontent" == dsovf.transport_iso9660(require_iso=False) self.m_mount_cb.assert_called_with( "/dev/xvdz", dsovf.get_ovf_env, mtype=None @@ -408,48 +393,47 @@ def test_mount_cb_called_require_iso_false(self): def test_maybe_cdrom_device_none(self): """Test maybe_cdrom_device returns False for none/empty input""" - self.assertFalse(dsovf.maybe_cdrom_device(None)) - self.assertFalse(dsovf.maybe_cdrom_device("")) + assert not dsovf.maybe_cdrom_device(None) + assert not dsovf.maybe_cdrom_device("") def test_maybe_cdrom_device_non_string_exception(self): """Test maybe_cdrom_device raises ValueError on non-string types""" - with self.assertRaises(ValueError): + with pytest.raises(ValueError): dsovf.maybe_cdrom_device({"a": "eleven"}) def test_maybe_cdrom_device_false_on_multi_dir_paths(self): """Test maybe_cdrom_device is false on /dev[/.*]/* paths""" - self.assertFalse(dsovf.maybe_cdrom_device("/dev/foo/sr0")) - self.assertFalse(dsovf.maybe_cdrom_device("foo/sr0")) - self.assertFalse(dsovf.maybe_cdrom_device("../foo/sr0")) - self.assertFalse(dsovf.maybe_cdrom_device("../foo/sr0")) + assert not dsovf.maybe_cdrom_device("/dev/foo/sr0") + assert not dsovf.maybe_cdrom_device("foo/sr0") + assert not dsovf.maybe_cdrom_device("../foo/sr0") + assert not dsovf.maybe_cdrom_device("../foo/sr0") def test_maybe_cdrom_device_true_on_hd_partitions(self): """Test maybe_cdrom_device is false on /dev/hd[a-z][0-9]+ paths""" - self.assertTrue(dsovf.maybe_cdrom_device("/dev/hda1")) - self.assertTrue(dsovf.maybe_cdrom_device("hdz9")) + assert dsovf.maybe_cdrom_device("/dev/hda1") + assert dsovf.maybe_cdrom_device("hdz9") def test_maybe_cdrom_device_true_on_valid_relative_paths(self): """Test maybe_cdrom_device normalizes paths""" - self.assertTrue(dsovf.maybe_cdrom_device("/dev/wark/../sr9")) - self.assertTrue(dsovf.maybe_cdrom_device("///sr0")) - self.assertTrue(dsovf.maybe_cdrom_device("/sr0")) - self.assertTrue(dsovf.maybe_cdrom_device("//dev//hda")) + assert dsovf.maybe_cdrom_device("/dev/wark/../sr9") + assert dsovf.maybe_cdrom_device("///sr0") + assert dsovf.maybe_cdrom_device("/sr0") + assert dsovf.maybe_cdrom_device("//dev//hda") def test_maybe_cdrom_device_true_on_xvd_partitions(self): """Test maybe_cdrom_device returns true on xvd*""" - self.assertTrue(dsovf.maybe_cdrom_device("/dev/xvda")) - self.assertTrue(dsovf.maybe_cdrom_device("/dev/xvda1")) - self.assertTrue(dsovf.maybe_cdrom_device("xvdza1")) + assert dsovf.maybe_cdrom_device("/dev/xvda") + assert dsovf.maybe_cdrom_device("/dev/xvda1") + assert dsovf.maybe_cdrom_device("xvdza1") @mock.patch(MPATH + "subp.which") @mock.patch(MPATH + "subp.subp") -class TestTransportVmwareGuestinfo(CiTestCase): +class TestTransportVmwareGuestinfo: """Test the com.vmware.guestInfo transport implemented in transport_vmware_guestinfo.""" rpctool = "vmware-rpctool" - with_logs = True rpctool_path = "/not/important/vmware-rpctool" vmtoolsd_path = "/not/important/vmtoolsd" @@ -457,38 +441,32 @@ def test_without_vmware_rpctool_and_without_vmtoolsd_returns_notfound( self, m_subp, m_which ): m_which.side_effect = [None, None] - self.assertEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual( - 0, - m_subp.call_count, - "subp should not be called if no rpctool in path.", - ) + assert NOT_FOUND == dsovf.transport_vmware_guestinfo() + assert ( + 0 == m_subp.call_count + ), "subp should not be called if no rpctool in path." def test_without_vmware_rpctool_and_with_vmtoolsd_returns_found( self, m_subp, m_which ): m_which.side_effect = [self.vmtoolsd_path, None] m_subp.side_effect = [(fill_properties({}), "")] - self.assertNotEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual( - 1, - m_subp.call_count, - "subp should be called once bc/ rpctool missing.", - ) + assert NOT_FOUND != dsovf.transport_vmware_guestinfo() + assert ( + 1 == m_subp.call_count + ), "subp should be called once bc/ rpctool missing." - def test_notfound_on_exit_code_1(self, m_subp, m_which): + def test_notfound_on_exit_code_1(self, m_subp, m_which, caplog): """If vmware-rpctool exits 1, then must return not found.""" m_which.side_effect = [None, self.rpctool_path] m_subp.side_effect = subp.ProcessExecutionError( stdout="", stderr="No value found", exit_code=1, cmd=["unused"] ) - self.assertEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual(1, m_subp.call_count) - self.assertNotIn( - "WARNING", - self.logs.getvalue(), - "exit code of 1 by rpctool should not cause warning.", - ) + assert NOT_FOUND == dsovf.transport_vmware_guestinfo() + assert 1 == m_subp.call_count + assert ( + "WARNING" not in caplog.text + ), "exit code of 1 by rpctool should not cause warning." def test_notfound_if_no_content_but_exit_zero(self, m_subp, m_which): """If vmware-rpctool exited 0 with no stdout is normal not-found. @@ -499,32 +477,34 @@ def test_notfound_if_no_content_but_exit_zero(self, m_subp, m_which): """ m_which.side_effect = [None, self.rpctool_path] m_subp.return_value = ("", "") - self.assertEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual(1, m_subp.call_count) + assert NOT_FOUND == dsovf.transport_vmware_guestinfo() + assert 1 == m_subp.call_count - def test_notfound_and_warns_on_unexpected_exit_code(self, m_subp, m_which): + def test_notfound_and_warns_on_unexpected_exit_code( + self, m_subp, m_which, caplog + ): """If vmware-rpctool exits non zero or 1, warnings should be logged.""" m_which.side_effect = [None, self.rpctool_path] m_subp.side_effect = subp.ProcessExecutionError( stdout=None, stderr="No value found", exit_code=2, cmd=["unused"] ) - self.assertEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual(1, m_subp.call_count) - self.assertIn( - "WARNING", - self.logs.getvalue(), - "exit code of 2 by rpctool should log WARNING.", - ) + assert NOT_FOUND == dsovf.transport_vmware_guestinfo() + assert 1 == m_subp.call_count + assert ( + "WARNING" in caplog.text + ), "exit code of 2 by rpctool should log WARNING." def test_found_when_guestinfo_present(self, m_subp, m_which): """When there is a ovf info, transport should return it.""" m_which.side_effect = [None, self.rpctool_path] content = fill_properties({}) m_subp.return_value = (content, "") - self.assertEqual(content, dsovf.transport_vmware_guestinfo()) - self.assertEqual(1, m_subp.call_count) + assert content == dsovf.transport_vmware_guestinfo() + assert 1 == m_subp.call_count - def test_vmware_rpctool_fails_and_vmtoolsd_fails(self, m_subp, m_which): + def test_vmware_rpctool_fails_and_vmtoolsd_fails( + self, m_subp, m_which, caplog + ): """When vmware-rpctool fails and vmtoolsd fails""" m_which.side_effect = [self.vmtoolsd_path, self.rpctool_path] m_subp.side_effect = [ @@ -535,13 +515,11 @@ def test_vmware_rpctool_fails_and_vmtoolsd_fails(self, m_subp, m_which): stdout="", stderr="No value found", exit_code=1, cmd=["unused"] ), ] - self.assertEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual(2, m_subp.call_count) - self.assertNotIn( - "WARNING", - self.logs.getvalue(), - "exit code of 1 by rpctool and vmtoolsd should not cause warning.", - ) + assert NOT_FOUND == dsovf.transport_vmware_guestinfo() + assert 2 == m_subp.call_count + assert ( + "WARNING" not in caplog.text + ), "exit code of 1 by rpctool and vmtoolsd should not cause warning." def test_vmware_rpctool_fails_and_vmtoolsd_success(self, m_subp, m_which): """When vmware-rpctool fails but vmtoolsd succeeds""" @@ -552,8 +530,8 @@ def test_vmware_rpctool_fails_and_vmtoolsd_success(self, m_subp, m_which): ), (fill_properties({}), ""), ] - self.assertNotEqual(NOT_FOUND, dsovf.transport_vmware_guestinfo()) - self.assertEqual(2, m_subp.call_count) + assert NOT_FOUND != dsovf.transport_vmware_guestinfo() + assert 2 == m_subp.call_count # From df91756001363c52324fb2234511722d38a42fd0 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 14 Aug 2025 18:59:31 +0200 Subject: [PATCH 074/150] test: drop citestcase from v sources (#6372) Co-authored-by: Chad Smith --- tests/unittests/sources/test_vultr.py | 101 ++-- .../sources/vmware/test_custom_script.py | 123 +++-- .../sources/vmware/test_guestcust_util.py | 40 +- .../sources/vmware/test_vmware_config_file.py | 448 ++++++++---------- 4 files changed, 300 insertions(+), 412 deletions(-) diff --git a/tests/unittests/sources/test_vultr.py b/tests/unittests/sources/test_vultr.py index e5f1c39ecae..2cf555a0fdb 100644 --- a/tests/unittests/sources/test_vultr.py +++ b/tests/unittests/sources/test_vultr.py @@ -6,12 +6,15 @@ # https://www.vultr.com/metadata/ import copy +import json +from unittest import mock -from cloudinit import helpers, settings +import pytest + +from cloudinit import settings from cloudinit.net.dhcp import NoDHCPLeaseError from cloudinit.sources import DataSourceVultr from cloudinit.sources.helpers import vultr -from tests.unittests.helpers import CiTestCase, mock VENDOR_DATA = """\ #cloud-config @@ -64,7 +67,7 @@ } ], "public-keys": ["ssh-rsa AAAAB3NzaC1y...IQQhv5PAOKaIl+mM3c= test3@key"], - "region": "us", + "region": {"regioncode": "EWR", "countrycode": "US"}, "user-defined": [], "startup-script": "echo No configured startup script", "raid1-script": "", @@ -126,7 +129,7 @@ }, ], "public-keys": ["ssh-rsa AAAAB3NzaC1y...IQQhv5PAOKaIl+mM3c= test3@key"], - "region": "us", + "region": {"regioncode": "EWR", "countrycode": "US"}, "user-defined": [], "startup-script": "echo No configured startup script", "user-data": [], @@ -244,54 +247,30 @@ } -class TestDataSourceVultr(CiTestCase): - def setUp(self): - super(TestDataSourceVultr, self).setUp() - self.tmp = self.tmp_dir() +class TestDataSourceVultr: + @pytest.fixture + def ds(self, paths, tmp_path): + distro = mock.MagicMock() + distro.get_tmp_exec_path.return_value = str(tmp_path) + return DataSourceVultr.DataSourceVultr( + settings.CFG_BUILTIN, distro, paths + ) # Test the datasource itself @mock.patch("cloudinit.net.get_interfaces_by_mac") @mock.patch("cloudinit.sources.helpers.vultr.is_vultr") @mock.patch("cloudinit.sources.helpers.vultr.get_metadata") - def test_datasource(self, mock_getmeta, mock_isvultr, mock_netmap): + def test_datasource(self, mock_getmeta, mock_isvultr, mock_netmap, ds): mock_getmeta.return_value = VULTR_V1_2 mock_isvultr.return_value = True mock_netmap.return_value = INTERFACE_MAP - distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - source = DataSourceVultr.DataSourceVultr( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) - ) - - # Test for failure - self.assertEqual(True, source._get_data()) - - # Test instance id - self.assertEqual("42872224", source.metadata["instanceid"]) - - # Test hostname - self.assertEqual("CLOUDINIT_2", source.metadata["local-hostname"]) - - # Test ssh keys - self.assertEqual(SSH_KEYS_1, source.metadata["public-keys"]) - - # Test vendor data generation - orig_val = self.maxDiff - self.maxDiff = None - - vendordata = source.vendordata_raw - - # Test vendor config - self.assertEqual( - VENDOR_DATA, - vendordata, - ) - - self.maxDiff = orig_val - - # Test network config generation - self.assertEqual(EXPECTED_VULTR_NETWORK_2, source.network_config) + assert True is ds._get_data() + assert "42872224" == ds.metadata["instanceid"] + assert "CLOUDINIT_2" == ds.metadata["local-hostname"] + assert SSH_KEYS_1 == ds.metadata["public-keys"] + assert VENDOR_DATA == ds.vendordata_raw + assert EXPECTED_VULTR_NETWORK_2 == ds.network_config def _get_metadata(self): # Create v1_3 @@ -307,22 +286,16 @@ def _get_metadata(self): @mock.patch("cloudinit.sources.helpers.vultr.is_vultr") @mock.patch("cloudinit.sources.helpers.vultr.get_metadata") def test_datasource_cloud_interfaces( - self, mock_getmeta, mock_isvultr, mock_netmap + self, mock_getmeta, mock_isvultr, mock_netmap, ds ): mock_getmeta.return_value = self._get_metadata() mock_isvultr.return_value = True mock_netmap.return_value = INTERFACE_MAP - distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - source = DataSourceVultr.DataSourceVultr( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) - ) - - source._get_data() + ds._get_data() # Test network config generation - self.assertEqual(EXPECTED_VULTR_NETWORK_2, source.network_config) + assert EXPECTED_VULTR_NETWORK_2 == ds.network_config # Test network config generation @mock.patch("cloudinit.net.get_interfaces_by_mac") @@ -330,8 +303,8 @@ def test_network_config(self, mock_netmap): mock_netmap.return_value = INTERFACE_MAP interf = VULTR_V1_1["interfaces"] - self.assertEqual( - EXPECTED_VULTR_NETWORK_1, vultr.generate_network_config(interf) + assert EXPECTED_VULTR_NETWORK_1 == vultr.generate_network_config( + interf ) # Test Private Networking config generation @@ -341,15 +314,15 @@ def test_private_network_config(self, mock_netmap): interf = copy.deepcopy(VULTR_V1_2["interfaces"]) # Test configuring - self.assertEqual( - EXPECTED_VULTR_NETWORK_2, vultr.generate_network_config(interf) + assert EXPECTED_VULTR_NETWORK_2 == vultr.generate_network_config( + interf ) # Test unconfigured interf[1]["unconfigured"] = True expected = copy.deepcopy(EXPECTED_VULTR_NETWORK_2) expected["config"].pop(2) - self.assertEqual(expected, vultr.generate_network_config(interf)) + assert expected == vultr.generate_network_config(interf) # Override ephemeral for proper unit testing def override_enter(self): @@ -379,20 +352,12 @@ def test_interface_seek( mock_read_metadata, mock_isvultr, mock_eph_init, + ds, ): - mock_read_metadata.return_value = {} + mock_read_metadata.return_value = json.dumps(VULTR_V1_1) mock_isvultr.return_value = True mock_interface_list.return_value = FILTERED_INTERFACES - distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - source = DataSourceVultr.DataSourceVultr( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) - ) - - try: - source._get_data() - except Exception: - pass + ds.get_metadata() assert mock_eph_init.call_args[1]["iface"] == FILTERED_INTERFACES[1] diff --git a/tests/unittests/sources/vmware/test_custom_script.py b/tests/unittests/sources/vmware/test_custom_script.py index 530fbc682be..b886f124d3b 100644 --- a/tests/unittests/sources/vmware/test_custom_script.py +++ b/tests/unittests/sources/vmware/test_custom_script.py @@ -7,6 +7,9 @@ import os import stat +from unittest import mock + +import pytest from cloudinit import util from cloudinit.sources.helpers.vmware.imc.config_custom_script import ( @@ -15,17 +18,24 @@ PostCustomScript, PreCustomScript, ) -from tests.unittests.helpers import CiTestCase, mock -class TestVmwareCustomScript(CiTestCase): - def setUp(self): - self.tmpDir = self.tmp_dir() - # Mock the tmpDir as the root dir in VM. - self.execDir = os.path.join(self.tmpDir, ".customization") - self.execScript = os.path.join(self.execDir, ".customize.sh") +@pytest.fixture +def fake_exec_dir(mocker, tmp_path): + exec_dir = tmp_path / ".customization" + mocker.patch.object(CustomScriptConstant, "CUSTOM_TMP_DIR", str(exec_dir)) + return exec_dir + + +@pytest.fixture +def fake_exec_script(fake_exec_dir, mocker): + ex_script = fake_exec_dir / ".customize.sh" + mocker.patch.object(CustomScriptConstant, "CUSTOM_SCRIPT", str(ex_script)) + return ex_script + - def test_prepare_custom_script(self): +class TestVmwareCustomScript: + def test_prepare_custom_script(self, fake_exec_script, tmp_path): """ This test is designed to verify the behavior based on the presence of custom script. Mainly needed for scenario where a custom script is @@ -33,79 +43,60 @@ def test_prepare_custom_script(self): is raised in such cases. """ # Custom script does not exist. - preCust = PreCustomScript("random-vmw-test", self.tmpDir) - self.assertEqual("random-vmw-test", preCust.scriptname) - self.assertEqual(self.tmpDir, preCust.directory) - self.assertEqual( - self.tmp_path("random-vmw-test", self.tmpDir), preCust.scriptpath - ) - with self.assertRaises(CustomScriptNotFound): + preCust = PreCustomScript("random-vmw-test", str(tmp_path)) + assert "random-vmw-test" == preCust.scriptname + assert str(tmp_path) == preCust.directory + assert str(tmp_path / "random-vmw-test") == preCust.scriptpath + with pytest.raises(CustomScriptNotFound): preCust.prepare_script() # Custom script exists. - custScript = self.tmp_path("test-cust", self.tmpDir) + custScript = str(tmp_path / "test-cust") util.write_file(custScript, "test-CR-strip\r\r") - with mock.patch.object( - CustomScriptConstant, "CUSTOM_TMP_DIR", self.execDir - ): - with mock.patch.object( - CustomScriptConstant, "CUSTOM_SCRIPT", self.execScript - ): - postCust = PostCustomScript( - "test-cust", self.tmpDir, self.tmpDir - ) - self.assertEqual("test-cust", postCust.scriptname) - self.assertEqual(self.tmpDir, postCust.directory) - self.assertEqual(custScript, postCust.scriptpath) - postCust.prepare_script() - # Custom script is copied with exec privilege - self.assertTrue(os.path.exists(self.execScript)) - st = os.stat(self.execScript) - self.assertTrue(st.st_mode & stat.S_IEXEC) - with open(self.execScript, "r") as f: - content = f.read() - self.assertEqual(content, "test-CR-strip") - # Check if all carraige returns are stripped from script. - self.assertFalse("\r" in content) + postCust = PostCustomScript("test-cust", str(tmp_path), str(tmp_path)) + assert "test-cust" == postCust.scriptname + assert str(tmp_path) == preCust.directory + assert custScript == postCust.scriptpath + postCust.prepare_script() - def test_execute_post_cust(self): + # Custom script is copied with exec privilege + assert fake_exec_script.exists() + st = os.stat(fake_exec_script) + assert st.st_mode & stat.S_IEXEC + assert "test-CR-strip" == fake_exec_script.read_text() + + def test_execute_post_cust(self, fake_exec_script, tmp_path): """ This test is designed to verify the behavior after execute post customization. """ # Prepare the customize package - postCustRun = self.tmp_path("post-customize-guest.sh", self.tmpDir) + postCustRun = str(tmp_path / "post-customize-guest.sh") util.write_file(postCustRun, "This is the script to run post cust") - userScript = self.tmp_path("test-cust", self.tmpDir) + userScript = str(tmp_path / "test-cust") util.write_file(userScript, "This is the post cust script") # Mock the cc_scripts_per_instance dir and marker file. # Create another tmp dir for cc_scripts_per_instance. - ccScriptDir = self.tmp_dir() - ccScript = os.path.join(ccScriptDir, "post-customize-guest.sh") - markerFile = os.path.join(self.tmpDir, ".markerFile") + ccScriptDir = tmp_path / "out" + ccScriptDir.mkdir() + ccScript = ccScriptDir / "post-customize-guest.sh" + markerFile = tmp_path / ".markerFile" + with mock.patch.object( - CustomScriptConstant, "CUSTOM_TMP_DIR", self.execDir + CustomScriptConstant, + "POST_CUSTOM_PENDING_MARKER", + str(markerFile), ): - with mock.patch.object( - CustomScriptConstant, "CUSTOM_SCRIPT", self.execScript - ): - with mock.patch.object( - CustomScriptConstant, - "POST_CUSTOM_PENDING_MARKER", - markerFile, - ): - postCust = PostCustomScript( - "test-cust", self.tmpDir, ccScriptDir - ) - postCust.execute() - # Check cc_scripts_per_instance and marker file - # are created. - self.assertTrue(os.path.exists(ccScript)) - with open(ccScript, "r") as f: - content = f.read() - self.assertEqual( - content, "This is the script to run post cust" - ) - self.assertTrue(os.path.exists(markerFile)) + postCust = PostCustomScript( + "test-cust", str(tmp_path), ccScriptDir + ) + postCust.execute() + # Check cc_scripts_per_instance and marker file + # are created. + assert ccScript.exists() + assert ( + "This is the script to run post cust" == ccScript.read_text() + ) + assert markerFile.exists() diff --git a/tests/unittests/sources/vmware/test_guestcust_util.py b/tests/unittests/sources/vmware/test_guestcust_util.py index 4e7541e9062..46d81e076f6 100644 --- a/tests/unittests/sources/vmware/test_guestcust_util.py +++ b/tests/unittests/sources/vmware/test_guestcust_util.py @@ -5,6 +5,8 @@ # # This file is part of cloud-init. See LICENSE file for license information. +from unittest import mock + from cloudinit import subp from cloudinit.sources.helpers.vmware.imc.config import Config from cloudinit.sources.helpers.vmware.imc.config_file import ConfigFile @@ -13,18 +15,18 @@ set_gc_status, ) from cloudinit.subp import SubpResult -from tests.unittests.helpers import CiTestCase, mock -class TestGuestCustUtil(CiTestCase): +class TestGuestCustUtil: def test_get_tools_config_not_installed(self): """ This test is designed to verify the behavior if vmware-toolbox-cmd is not installed. """ with mock.patch.object(subp, "which", return_value=None): - self.assertEqual( - get_tools_config("section", "key", "defaultVal"), "defaultVal" + assert ( + get_tools_config("section", "key", "defaultVal") + == "defaultVal" ) def test_get_tools_config_internal_exception(self): @@ -42,9 +44,9 @@ def test_get_tools_config_internal_exception(self): ), ): # verify return value is 'defaultVal', not 'value'. - self.assertEqual( - get_tools_config("section", "key", "defaultVal"), - "defaultVal", + assert ( + get_tools_config("section", "key", "defaultVal") + == "defaultVal" ) def test_get_tools_config_normal(self): @@ -57,31 +59,29 @@ def test_get_tools_config_normal(self): with mock.patch.object( subp, "subp", return_value=SubpResult("key = value ", b"") ): - self.assertEqual( - get_tools_config("section", "key", "defaultVal"), "value" + assert ( + get_tools_config("section", "key", "defaultVal") == "value" ) # value is blank with mock.patch.object( subp, "subp", return_value=SubpResult("key = ", b"") ): - self.assertEqual( - get_tools_config("section", "key", "defaultVal"), "" - ) + assert get_tools_config("section", "key", "defaultVal") == "" # value contains = with mock.patch.object( subp, "subp", return_value=SubpResult("key=Bar=Wark", b"") ): - self.assertEqual( - get_tools_config("section", "key", "defaultVal"), - "Bar=Wark", + assert ( + get_tools_config("section", "key", "defaultVal") + == "Bar=Wark" ) # value contains specific characters with mock.patch.object( subp, "subp", return_value=SubpResult("[a] b.c_d=e-f", b"") ): - self.assertEqual( - get_tools_config("section", "key", "defaultVal"), "e-f" + assert ( + get_tools_config("section", "key", "defaultVal") == "e-f" ) def test_set_gc_status(self): @@ -89,12 +89,12 @@ def test_set_gc_status(self): This test is designed to verify the behavior of set_gc_status """ # config is None, return None - self.assertEqual(set_gc_status(None, "Successful"), None) + assert set_gc_status(None, "Successful") is None # post gc status is NO, return None cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - self.assertEqual(set_gc_status(conf, "Successful"), None) + assert set_gc_status(conf, "Successful") is None # post gc status is YES, subp is called to execute command cf._insertKey("MISC|POST-GC-STATUS", "YES") @@ -102,7 +102,7 @@ def test_set_gc_status(self): with mock.patch.object( subp, "subp", return_value=SubpResult("ok", b"") ) as mockobj: - self.assertEqual(set_gc_status(conf, "Successful"), ("ok", b"")) + assert set_gc_status(conf, "Successful") == ("ok", b"") mockobj.assert_called_once_with( ["vmware-rpctool", "info-set guestinfo.gc.status Successful"], rcs=[0], diff --git a/tests/unittests/sources/vmware/test_vmware_config_file.py b/tests/unittests/sources/vmware/test_vmware_config_file.py index 837efa218d3..c9b23287385 100644 --- a/tests/unittests/sources/vmware/test_vmware_config_file.py +++ b/tests/unittests/sources/vmware/test_vmware_config_file.py @@ -9,11 +9,11 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging -import os import sys -import tempfile import textwrap +import pytest + from cloudinit.sources.helpers.vmware.imc.boot_proto import BootProtoEnum from cloudinit.sources.helpers.vmware.imc.config import Config from cloudinit.sources.helpers.vmware.imc.config_file import ( @@ -25,7 +25,6 @@ get_non_network_data_from_vmware_cust_cfg, ) from tests.helpers import cloud_init_project_dir -from tests.unittests.helpers import CiTestCase logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) logger = logging.getLogger(__name__) @@ -35,23 +34,22 @@ def ConfigFile(path: str): return WrappedConfigFile(cloud_init_project_dir(path)) -class TestVmwareConfigFile(CiTestCase): +class TestVmwareConfigFile: def test_utility_methods(self): """Tests basic utility methods of ConfigFile class""" cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") cf.clear() - - self.assertEqual(0, len(cf), "clear size") + assert 0 == len(cf), "clear size" cf._insertKey(" PASSWORD|-PASS ", " foo ") cf._insertKey("BAR", " ") - self.assertEqual(2, len(cf), "insert size") - self.assertEqual("foo", cf["PASSWORD|-PASS"], "password") - self.assertTrue("PASSWORD|-PASS" in cf, "hasPassword") - self.assertFalse("FOO" in cf, "hasFoo") - self.assertTrue("BAR" in cf, "hasBar") + assert 2 == len(cf), "insert size" + assert "foo" == cf["PASSWORD|-PASS"], "password" + assert "PASSWORD|-PASS" in cf, "hasPassword" + assert "FOO" not in cf, "hasFoo" + assert "BAR" in cf, "hasBar" def test_configfile_without_instance_id(self): """ @@ -60,11 +58,11 @@ def test_configfile_without_instance_id(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - (md1, _) = get_non_network_data_from_vmware_cust_cfg(conf) - self.assertFalse("instance-id" in md1) + md1, _ = get_non_network_data_from_vmware_cust_cfg(conf) + assert "instance-id" not in md1 (md2, _) = get_non_network_data_from_vmware_cust_cfg(conf) - self.assertFalse("instance-id" in md2) + assert "instance-id" not in md2 def test_configfile_with_instance_id(self): """Tests instance id get from configuration file""" @@ -72,10 +70,10 @@ def test_configfile_with_instance_id(self): conf = Config(cf) (md1, _) = get_non_network_data_from_vmware_cust_cfg(conf) - self.assertEqual(md1["instance-id"], conf.instance_id, "instance-id") + assert md1["instance-id"] == conf.instance_id, "instance-id" (md2, _) = get_non_network_data_from_vmware_cust_cfg(conf) - self.assertEqual(md2["instance-id"], conf.instance_id, "instance-id") + assert md2["instance-id"] == conf.instance_id, "instance-id" def test_configfile_static_2nics(self): """Tests Config class for a configuration with two static NICs.""" @@ -83,38 +81,33 @@ def test_configfile_static_2nics(self): conf = Config(cf) - self.assertEqual("myhost1", conf.host_name, "hostName") - self.assertEqual("Africa/Abidjan", conf.timezone, "tz") + assert "myhost1" == conf.host_name, "hostName" + assert "Africa/Abidjan" == conf.timezone, "tz" - self.assertEqual( - ["10.20.145.1", "10.20.145.2"], conf.name_servers, "dns" - ) - self.assertEqual( - ["eng.vmware.com", "proxy.vmware.com"], - conf.dns_suffixes, - "suffixes", - ) + assert ["10.20.145.1", "10.20.145.2"] == conf.name_servers, "dns" + assert [ + "eng.vmware.com", + "proxy.vmware.com", + ] == conf.dns_suffixes, "suffixes" nics = conf.nics ipv40 = nics[0].staticIpv4 - self.assertEqual(2, len(nics), "nics") - self.assertEqual("NIC1", nics[0].name, "nic0") - self.assertEqual("00:50:56:a6:8c:08", nics[0].mac, "mac0") - self.assertEqual(BootProtoEnum.STATIC, nics[0].bootProto, "bootproto0") - self.assertEqual("10.20.87.154", ipv40[0].ip, "ipv4Addr0") - self.assertEqual("255.255.252.0", ipv40[0].netmask, "ipv4Mask0") - self.assertEqual(2, len(ipv40[0].gateways), "ipv4Gw0") - self.assertEqual("10.20.87.253", ipv40[0].gateways[0], "ipv4Gw0_0") - self.assertEqual("10.20.87.105", ipv40[0].gateways[1], "ipv4Gw0_1") - - self.assertEqual(1, len(nics[0].staticIpv6), "ipv6Cnt0") - self.assertEqual( - "fc00:10:20:87::154", nics[0].staticIpv6[0].ip, "ipv6Addr0" - ) + assert 2 == len(nics), "nics" + assert "NIC1" == nics[0].name, "nic0" + assert "00:50:56:a6:8c:08" == nics[0].mac, "mac0" + assert BootProtoEnum.STATIC == nics[0].bootProto, "bootproto0" + assert "10.20.87.154" == ipv40[0].ip, "ipv4Addr0" + assert "255.255.252.0" == ipv40[0].netmask, "ipv4Mask0" + assert 2 == len(ipv40[0].gateways), "ipv4Gw0" + assert "10.20.87.253" == ipv40[0].gateways[0], "ipv4Gw0_0" + assert "10.20.87.105" == ipv40[0].gateways[1], "ipv4Gw0_1" - self.assertEqual("NIC2", nics[1].name, "nic1") - self.assertTrue(not nics[1].staticIpv6, "ipv61 dhcp") + assert 1 == len(nics[0].staticIpv6), "ipv6Cnt0" + assert "fc00:10:20:87::154" == nics[0].staticIpv6[0].ip, "ipv6Addr0" + + assert "NIC2" == nics[1].name, "nic1" + assert not nics[1].staticIpv6, "ipv61 dhcp" def test_config_file_dhcp_2nics(self): """Tests Config class for a configuration with two DHCP NICs.""" @@ -122,10 +115,10 @@ def test_config_file_dhcp_2nics(self): conf = Config(cf) nics = conf.nics - self.assertEqual(2, len(nics), "nics") - self.assertEqual("NIC1", nics[0].name, "nic0") - self.assertEqual("00:50:56:a6:8c:08", nics[0].mac, "mac0") - self.assertEqual(BootProtoEnum.DHCP, nics[0].bootProto, "bootproto0") + assert 2 == len(nics), "nics" + assert "NIC1" == nics[0].name, "nic0" + assert "00:50:56:a6:8c:08" == nics[0].mac, "mac0" + assert BootProtoEnum.DHCP == nics[0].bootProto, "bootproto0" def test_config_password(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") @@ -134,8 +127,8 @@ def test_config_password(self): cf._insertKey("PASSWORD|RESET", "no") conf = Config(cf) - self.assertEqual("test-password", conf.admin_password, "password") - self.assertFalse(conf.reset_password, "do not reset password") + assert "test-password" == conf.admin_password, "password" + assert not conf.reset_password, "do not reset password" def test_config_reset_passwd(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") @@ -144,16 +137,15 @@ def test_config_reset_passwd(self): cf._insertKey("PASSWORD|RESET", "random") conf = Config(cf) - with self.assertRaises(ValueError): - pw = conf.reset_password - self.assertIsNone(pw) + with pytest.raises(ValueError): + assert None is conf.reset_password cf.clear() cf._insertKey("PASSWORD|RESET", "yes") - self.assertEqual(1, len(cf), "insert size") + assert 1 == len(cf), "insert size" conf = Config(cf) - self.assertTrue(conf.reset_password, "reset password") + assert conf.reset_password, "reset password" def test_get_config_nameservers(self): """Tests DNS and nameserver settings in a configuration.""" @@ -163,22 +155,19 @@ def test_get_config_nameservers(self): network_config = get_network_data_from_vmware_cust_cfg(config, False) - self.assertEqual(2, network_config.get("version")) + assert 2 == network_config.get("version") ethernets = network_config.get("ethernets") for _, config in ethernets.items(): - self.assertTrue(isinstance(config, dict)) + assert isinstance(config, dict) name_servers = config.get("nameservers").get("addresses") dns_suffixes = config.get("nameservers").get("search") - self.assertEqual( - ["10.20.145.1", "10.20.145.2"], name_servers, "dns" - ) - self.assertEqual( - ["eng.vmware.com", "proxy.vmware.com"], - dns_suffixes, - "suffixes", - ) + assert ["10.20.145.1", "10.20.145.2"] == name_servers, "dns" + assert [ + "eng.vmware.com", + "proxy.vmware.com", + ] == dns_suffixes, "suffixes" def test_get_config_dns_suffixes(self): """Tests if get_network_from_vmware_cust_cfg properly @@ -190,16 +179,16 @@ def test_get_config_dns_suffixes(self): network_config = get_network_data_from_vmware_cust_cfg(config, False) - self.assertEqual(2, network_config.get("version")) + assert 2 == network_config.get("version") ethernets = network_config.get("ethernets") for _, config in ethernets.items(): - self.assertTrue(isinstance(config, dict)) + assert isinstance(config, dict) name_servers = config.get("nameservers").get("addresses") dns_suffixes = config.get("nameservers").get("search") - self.assertEqual(None, name_servers, "dns") - self.assertEqual(["eng.vmware.com"], dns_suffixes, "suffixes") + assert None is name_servers, "dns" + assert ["eng.vmware.com"] == dns_suffixes, "suffixes" def test_get_nics_list_dhcp(self): """Tests if NicConfigurator properly calculates ethernets @@ -213,44 +202,28 @@ def test_get_nics_list_dhcp(self): ) ethernets_dict = nicConfigurator.generate() - self.assertTrue(isinstance(ethernets_dict, dict)) - self.assertEqual(2, len(ethernets_dict), "number of ethernets") + assert isinstance(ethernets_dict, dict) + assert 2 == len(ethernets_dict), "number of ethernets" for name, config in ethernets_dict.items(): if name == "NIC1": - self.assertEqual( - "00:50:56:a6:8c:08", - config.get("match").get("macaddress"), - "mac address of NIC1", - ) - self.assertEqual( - True, config.get("wakeonlan"), "wakeonlan of NIC1" - ) - self.assertEqual( - True, config.get("dhcp4"), "DHCPv4 enablement of NIC1" - ) - self.assertEqual( - False, - config.get("dhcp4-overrides").get("use-dns"), - "use-dns enablement for dhcp4-overrides of NIC1", - ) + assert "00:50:56:a6:8c:08" == config.get("match").get( + "macaddress" + ), "mac address of NIC1" + assert True is config.get("wakeonlan"), "wakeonlan of NIC1" + assert True is config.get("dhcp4"), "DHCPv4 enablement of NIC1" + assert False is config.get("dhcp4-overrides").get( + "use-dns" + ), "use-dns enablement for dhcp4-overrides of NIC1" if name == "NIC2": - self.assertEqual( - "00:50:56:a6:5a:de", - config.get("match").get("macaddress"), - "mac address of NIC2", - ) - self.assertEqual( - True, config.get("wakeonlan"), "wakeonlan of NIC2" - ) - self.assertEqual( - True, config.get("dhcp4"), "DHCPv4 enablement of NIC2" - ) - self.assertEqual( - False, - config.get("dhcp4-overrides").get("use-dns"), - "use-dns enablement for dhcp4-overrides of NIC2", - ) + assert "00:50:56:a6:5a:de" == config.get("match").get( + "macaddress" + ), "mac address of NIC2" + assert True is config.get("wakeonlan"), "wakeonlan of NIC2" + assert True is config.get("dhcp4"), "DHCPv4 enablement of NIC2" + assert False is config.get("dhcp4-overrides").get( + "use-dns" + ), "use-dns enablement for dhcp4-overrides of NIC2" def test_get_nics_list_static(self): """Tests if NicConfigurator properly calculates ethernets @@ -264,124 +237,95 @@ def test_get_nics_list_static(self): ) ethernets_dict = nicConfigurator.generate() - self.assertTrue(isinstance(ethernets_dict, dict)) - self.assertEqual(2, len(ethernets_dict), "number of ethernets") + assert isinstance(ethernets_dict, dict) + assert 2 == len(ethernets_dict), "number of ethernets" for name, config in ethernets_dict.items(): print(config) if name == "NIC1": - self.assertEqual( - "00:50:56:a6:8c:08", - config.get("match").get("macaddress"), - "mac address of NIC1", - ) - self.assertEqual( - True, config.get("wakeonlan"), "wakeonlan of NIC1" - ) - self.assertEqual( - False, config.get("dhcp4"), "DHCPv4 enablement of NIC1" - ) - self.assertEqual( - False, config.get("dhcp6"), "DHCPv6 enablement of NIC1" - ) - self.assertEqual( - ["10.20.87.154/22", "fc00:10:20:87::154/64"], - config.get("addresses"), - "IP addresses of NIC1", - ) - self.assertEqual( - [ - {"to": "10.20.84.0/22", "via": "10.20.87.253"}, - {"to": "10.20.84.0/22", "via": "10.20.87.105"}, - { - "to": "fc00:10:20:87::/64", - "via": "fc00:10:20:87::253", - }, - ], - config.get("routes"), - "routes of NIC1", - ) + assert "00:50:56:a6:8c:08" == config.get("match").get( + "macaddress" + ), "mac address of NIC1" + assert True is config.get("wakeonlan"), "wakeonlan of NIC1" + assert False is config.get( + "dhcp4" + ), "DHCPv4 enablement of NIC1" + assert False is config.get( + "dhcp6" + ), "DHCPv6 enablement of NIC1" + assert [ + "10.20.87.154/22", + "fc00:10:20:87::154/64", + ] == config.get("addresses"), "IP addresses of NIC1" + assert [ + {"to": "10.20.84.0/22", "via": "10.20.87.253"}, + {"to": "10.20.84.0/22", "via": "10.20.87.105"}, + { + "to": "fc00:10:20:87::/64", + "via": "fc00:10:20:87::253", + }, + ] == config.get("routes"), "routes of NIC1" if name == "NIC2": - self.assertEqual( - "00:50:56:a6:ef:7d", - config.get("match").get("macaddress"), - "mac address of NIC2", - ) - self.assertEqual( - True, config.get("wakeonlan"), "wakeonlan of NIC2" - ) - self.assertEqual( - False, config.get("dhcp4"), "DHCPv4 enablement of NIC2" - ) - self.assertEqual( - ["192.168.6.102/16"], - config.get("addresses"), - "IP addresses of NIC2", - ) - self.assertEqual( - [ - {"to": "192.168.0.0/16", "via": "192.168.0.10"}, - ], - config.get("routes"), - "routes of NIC2", - ) + assert "00:50:56:a6:ef:7d" == config.get("match").get( + "macaddress" + ), "mac address of NIC2" + assert True is config.get("wakeonlan"), "wakeonlan of NIC2" + assert False is config.get( + "dhcp4" + ), "DHCPv4 enablement of NIC2" + assert ["192.168.6.102/16"] == config.get( + "addresses" + ), "IP addresses of NIC2" + assert [ + {"to": "192.168.0.0/16", "via": "192.168.0.10"}, + ] == config.get("routes"), "routes of NIC2" def test_custom_script(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - self.assertIsNone(conf.custom_script_name) + assert conf.custom_script_name is None cf._insertKey("CUSTOM-SCRIPT|SCRIPT-NAME", "test-script") conf = Config(cf) - self.assertEqual("test-script", conf.custom_script_name) + assert "test-script" == conf.custom_script_name def test_post_gc_status(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - self.assertFalse(conf.post_gc_status) + assert not conf.post_gc_status cf._insertKey("MISC|POST-GC-STATUS", "YES") conf = Config(cf) - self.assertTrue(conf.post_gc_status) + assert conf.post_gc_status def test_no_default_run_post_script(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - self.assertFalse(conf.default_run_post_script) + assert not conf.default_run_post_script cf._insertKey("MISC|DEFAULT-RUN-POST-CUST-SCRIPT", "NO") conf = Config(cf) - self.assertFalse(conf.default_run_post_script) + assert not conf.default_run_post_script def test_yes_default_run_post_script(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") cf._insertKey("MISC|DEFAULT-RUN-POST-CUST-SCRIPT", "yes") conf = Config(cf) - self.assertTrue(conf.default_run_post_script) + assert conf.default_run_post_script -class TestVmwareNetConfig(CiTestCase): +class TestVmwareNetConfig: """Test conversion of vmware config to cloud-init config.""" - maxDiff = None - - def _get_NicConfigurator(self, text): - fp = None - try: - with tempfile.NamedTemporaryFile( - mode="w", dir=self.tmp_dir(), delete=False - ) as fp: - fp.write(text) - fp.close() - cfg = Config(ConfigFile(fp.name)) - return NicConfigurator( - cfg.nics, - cfg.name_servers, - cfg.dns_suffixes, - use_system_devices=False, - ) - finally: - if fp: - os.unlink(fp.name) - - def test_static_nic_without_ipv4_netmask(self): + def _get_NicConfigurator(self, tmp_path, text): + tmp_file = tmp_path / ".config" + tmp_file.write_text(text) + cfg = Config(ConfigFile(tmp_file)) + return NicConfigurator( + cfg.nics, + cfg.name_servers, + cfg.dns_suffixes, + use_system_devices=False, + ) + + def test_static_nic_without_ipv4_netmask(self, tmp_path): """netmask is optional for static ipv4 configuration.""" config = textwrap.dedent( """\ @@ -402,21 +346,18 @@ def test_static_nic_without_ipv4_netmask(self): IPADDR = 10.20.87.154 """ ) - nc = self._get_NicConfigurator(config) - self.assertEqual( - { - "NIC1": { - "match": {"macaddress": "00:50:56:a6:8c:08"}, - "wakeonlan": True, - "dhcp4": False, - "addresses": ["10.20.87.154/32"], - "set-name": "NIC1", - } - }, - nc.generate(), - ) - - def test_static_nic_without_ipv6_netmask(self): + nc = self._get_NicConfigurator(tmp_path, config) + assert { + "NIC1": { + "match": {"macaddress": "00:50:56:a6:8c:08"}, + "wakeonlan": True, + "dhcp4": False, + "addresses": ["10.20.87.154/32"], + "set-name": "NIC1", + } + } == nc.generate() + + def test_static_nic_without_ipv6_netmask(self, tmp_path): """netmask is mandatory for static ipv6 configuration.""" config = textwrap.dedent( """\ @@ -438,11 +379,11 @@ def test_static_nic_without_ipv6_netmask(self): IPv6ADDR|1 = fc00:10:20:87::154 """ ) - nc = self._get_NicConfigurator(config) - with self.assertRaises(ValueError): + nc = self._get_NicConfigurator(tmp_path, config) + with pytest.raises(ValueError): nc.generate() - def test_non_primary_nic_with_gateway(self): + def test_non_primary_nic_with_gateway(self, tmp_path): """A non primary nic set can have a gateway.""" config = textwrap.dedent( """\ @@ -465,22 +406,19 @@ def test_non_primary_nic_with_gateway(self): GATEWAY = 10.20.87.253 """ ) - nc = self._get_NicConfigurator(config) - self.assertEqual( - { - "NIC1": { - "match": {"macaddress": "00:50:56:a6:8c:08"}, - "wakeonlan": True, - "dhcp4": False, - "addresses": ["10.20.87.154/22"], - "routes": [{"to": "10.20.84.0/22", "via": "10.20.87.253"}], - "set-name": "NIC1", - } - }, - nc.generate(), - ) - - def test_cust_non_primary_nic_with_gateway_(self): + nc = self._get_NicConfigurator(tmp_path, config) + assert { + "NIC1": { + "match": {"macaddress": "00:50:56:a6:8c:08"}, + "wakeonlan": True, + "dhcp4": False, + "addresses": ["10.20.87.154/22"], + "routes": [{"to": "10.20.84.0/22", "via": "10.20.87.253"}], + "set-name": "NIC1", + } + } == nc.generate() + + def test_cust_non_primary_nic_with_gateway_(self, tmp_path): """A customer non primary nic set can have a gateway.""" config = textwrap.dedent( """\ @@ -512,25 +450,22 @@ def test_cust_non_primary_nic_with_gateway_(self): UTC = yes """ ) - nc = self._get_NicConfigurator(config) - self.assertEqual( - { - "NIC1": { - "match": {"macaddress": "00:50:56:ac:d1:8a"}, - "wakeonlan": True, - "dhcp4": False, - "addresses": ["100.115.223.75/24"], - "routes": [ - {"to": "100.115.223.0/24", "via": "100.115.223.254"} - ], - "set-name": "NIC1", - "nameservers": {"addresses": ["8.8.8.8"]}, - } - }, - nc.generate(), - ) - - def test_a_primary_nic_with_gateway(self): + nc = self._get_NicConfigurator(tmp_path, config) + assert { + "NIC1": { + "match": {"macaddress": "00:50:56:ac:d1:8a"}, + "wakeonlan": True, + "dhcp4": False, + "addresses": ["100.115.223.75/24"], + "routes": [ + {"to": "100.115.223.0/24", "via": "100.115.223.254"} + ], + "set-name": "NIC1", + "nameservers": {"addresses": ["8.8.8.8"]}, + } + } == nc.generate() + + def test_a_primary_nic_with_gateway(self, tmp_path): """A primary nic set can have a gateway.""" config = textwrap.dedent( """\ @@ -554,33 +489,30 @@ def test_a_primary_nic_with_gateway(self): GATEWAY = 10.20.87.253 """ ) - nc = self._get_NicConfigurator(config) - self.assertEqual( - { - "NIC1": { - "match": {"macaddress": "00:50:56:a6:8c:08"}, - "wakeonlan": True, - "dhcp4": False, - "addresses": ["10.20.87.154/22"], - "routes": [{"to": "0.0.0.0/0", "via": "10.20.87.253"}], - "set-name": "NIC1", - } - }, - nc.generate(), - ) + nc = self._get_NicConfigurator(tmp_path, config) + assert { + "NIC1": { + "match": {"macaddress": "00:50:56:a6:8c:08"}, + "wakeonlan": True, + "dhcp4": False, + "addresses": ["10.20.87.154/22"], + "routes": [{"to": "0.0.0.0/0", "via": "10.20.87.253"}], + "set-name": "NIC1", + } + } == nc.generate() def test_meta_data(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - self.assertIsNone(conf.meta_data_name) + assert conf.meta_data_name is None cf._insertKey("CLOUDINIT|METADATA", "test-metadata") conf = Config(cf) - self.assertEqual("test-metadata", conf.meta_data_name) + assert "test-metadata" == conf.meta_data_name def test_user_data(self): cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg") conf = Config(cf) - self.assertIsNone(conf.user_data_name) + assert conf.user_data_name is None cf._insertKey("CLOUDINIT|USERDATA", "test-userdata") conf = Config(cf) - self.assertEqual("test-userdata", conf.user_data_name) + assert "test-userdata" == conf.user_data_name From c9e282a234ee7b53f53f91c8640fb1410c0fdeb7 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 14 Aug 2025 19:50:48 +0200 Subject: [PATCH 075/150] test: drop citestcase from r, s and u sources (#6373) Add bool type hint DataSourceScaleway.ds_detect. Assert returns False instead of None Co-authored-by: Chad Smith --- cloudinit/sources/DataSourceScaleway.py | 3 +- tests/unittests/sources/test_rbx.py | 140 ++++++-------- tests/unittests/sources/test_scaleway.py | 224 ++++++++++------------- tests/unittests/sources/test_upcloud.py | 142 ++++++-------- 4 files changed, 216 insertions(+), 293 deletions(-) diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py index 43b4b8bd6fa..b2e8740b68a 100644 --- a/cloudinit/sources/DataSourceScaleway.py +++ b/cloudinit/sources/DataSourceScaleway.py @@ -235,7 +235,7 @@ def _crawl_metadata(self): ) @staticmethod - def ds_detect(): + def ds_detect() -> bool: """ There are three ways to detect if you are on Scaleway: @@ -254,6 +254,7 @@ def ds_detect(): cmdline = util.get_cmdline() if "scaleway" in cmdline: return True + return False def _get_data(self): diff --git a/tests/unittests/sources/test_rbx.py b/tests/unittests/sources/test_rbx.py index 475bf4982a1..f239d542a2a 100644 --- a/tests/unittests/sources/test_rbx.py +++ b/tests/unittests/sources/test_rbx.py @@ -1,8 +1,11 @@ import json +from unittest import mock -from cloudinit import distros, helpers, subp +import pytest + +from cloudinit import distros, subp from cloudinit.sources import DataSourceRbxCloud as ds -from tests.unittests.helpers import CiTestCase, mock, populate_dir +from tests.unittests.helpers import populate_dir DS_PATH = "cloudinit.sources.DataSourceRbxCloud" @@ -76,54 +79,42 @@ } -class TestRbxDataSource(CiTestCase): - parsed_user = None - allowed_subp = ["bash"] - - def _fetch_distro(self, kind): - cls = distros.fetch(kind) - paths = helpers.Paths({}) - return cls(kind, {}, paths) - - def setUp(self): - super(TestRbxDataSource, self).setUp() - self.tmp = self.tmp_dir() - self.paths = helpers.Paths( - {"cloud_dir": self.tmp, "run_dir": self.tmp} - ) +class TestRbxDataSource: + @pytest.fixture + def fetch_distro(self, paths): + def _fetch_distro(kind): + cls = distros.fetch(kind) + return cls(kind, {}, paths) - # defaults for few tests - self.ds = ds.DataSourceRbxCloud - self.seed_dir = self.paths.seed_dir - self.sys_cfg = {"datasource": {"RbxCloud": {"dsmode": "local"}}} + return _fetch_distro - def test_seed_read_user_data_callback_empty_file(self): - populate_user_metadata(self.seed_dir, "") - populate_cloud_metadata(self.seed_dir, {}) - results = ds.read_user_data_callback(self.seed_dir) + def test_seed_read_user_data_callback_empty_file(self, paths): + populate_user_metadata(paths.seed_dir, "") + populate_cloud_metadata(paths.seed_dir, {}) + results = ds.read_user_data_callback(paths.seed_dir) - self.assertIsNone(results) + assert results is None - def test_seed_read_user_data_callback_valid_disk(self): - populate_user_metadata(self.seed_dir, "") - populate_cloud_metadata(self.seed_dir, CLOUD_METADATA) - results = ds.read_user_data_callback(self.seed_dir) + def test_seed_read_user_data_callback_valid_disk(self, paths): + populate_user_metadata(paths.seed_dir, "") + populate_cloud_metadata(paths.seed_dir, CLOUD_METADATA) + results = ds.read_user_data_callback(paths.seed_dir) - self.assertNotEqual(results, None) - self.assertTrue("userdata" in results) - self.assertTrue("metadata" in results) - self.assertTrue("cfg" in results) + assert results is not None + assert "userdata" in results + assert "metadata" in results + assert "cfg" in results - def test_seed_read_user_data_callback_userdata(self): + def test_seed_read_user_data_callback_userdata(self, paths): userdata = "#!/bin/sh\nexit 1" - populate_user_metadata(self.seed_dir, userdata) - populate_cloud_metadata(self.seed_dir, CLOUD_METADATA) + populate_user_metadata(paths.seed_dir, userdata) + populate_cloud_metadata(paths.seed_dir, CLOUD_METADATA) - results = ds.read_user_data_callback(self.seed_dir) + results = ds.read_user_data_callback(paths.seed_dir) - self.assertNotEqual(results, None) - self.assertTrue("userdata" in results) - self.assertEqual(results["userdata"], userdata) + assert results is not None + assert "userdata" in results + assert results["userdata"] == userdata def test_generate_network_config(self): expected = { @@ -161,12 +152,10 @@ def test_generate_network_config(self): }, ], } - self.assertTrue( - ds.generate_network_config(CLOUD_METADATA["netadp"]), expected - ) + assert ds.generate_network_config(CLOUD_METADATA["netadp"]), expected @mock.patch(DS_PATH + ".subp.subp") - def test_gratuitous_arp_run_standard_arping(self, m_subp): + def test_gratuitous_arp_run_standard_arping(self, m_subp, fetch_distro): """Test handle run arping & parameters.""" items = [ {"destination": "172.17.0.2", "source": "172.16.6.104"}, @@ -175,21 +164,18 @@ def test_gratuitous_arp_run_standard_arping(self, m_subp): "source": "172.16.6.104", }, ] - ds.gratuitous_arp(items, self._fetch_distro("ubuntu")) - self.assertEqual( - [ - mock.call( - ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] - ), - mock.call( - ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] - ), - ], - m_subp.call_args_list, - ) + ds.gratuitous_arp(items, fetch_distro("ubuntu")) + assert [ + mock.call( + ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] + ), + mock.call( + ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] + ), + ] == m_subp.call_args_list @mock.patch(DS_PATH + ".subp.subp") - def test_handle_rhel_like_arping(self, m_subp): + def test_handle_rhel_like_arping(self, m_subp, fetch_distro): """Test handle on RHEL-like distros.""" items = [ { @@ -197,20 +183,17 @@ def test_handle_rhel_like_arping(self, m_subp): "destination": "172.17.0.2", } ] - ds.gratuitous_arp(items, self._fetch_distro("fedora")) - self.assertEqual( - [ - mock.call( - ["arping", "-c", "2", "-s", "172.16.6.104", "172.17.0.2"] - ) - ], - m_subp.call_args_list, - ) + ds.gratuitous_arp(items, fetch_distro("fedora")) + assert [ + mock.call( + ["arping", "-c", "2", "-s", "172.16.6.104", "172.17.0.2"] + ) + ] == m_subp.call_args_list @mock.patch( DS_PATH + ".subp.subp", side_effect=subp.ProcessExecutionError() ) - def test_continue_on_arping_error(self, m_subp): + def test_continue_on_arping_error(self, m_subp, fetch_distro): """Continue when command error""" items = [ {"destination": "172.17.0.2", "source": "172.16.6.104"}, @@ -219,18 +202,15 @@ def test_continue_on_arping_error(self, m_subp): "source": "172.16.6.104", }, ] - ds.gratuitous_arp(items, self._fetch_distro("ubuntu")) - self.assertEqual( - [ - mock.call( - ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] - ), - mock.call( - ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] - ), - ], - m_subp.call_args_list, - ) + ds.gratuitous_arp(items, fetch_distro("ubuntu")) + assert [ + mock.call( + ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] + ), + mock.call( + ["arping", "-c", "2", "-S", "172.16.6.104", "172.17.0.2"] + ), + ] == m_subp.call_args_list def populate_cloud_metadata(path, data): diff --git a/tests/unittests/sources/test_scaleway.py b/tests/unittests/sources/test_scaleway.py index bb1f2136de3..cc99ba742a6 100644 --- a/tests/unittests/sources/test_scaleway.py +++ b/tests/unittests/sources/test_scaleway.py @@ -4,18 +4,15 @@ import socket from urllib.parse import SplitResult, urlsplit +import pytest import requests import responses from requests.exceptions import ConnectionError, ConnectTimeout -from cloudinit import helpers, settings, sources +from cloudinit import settings, sources from cloudinit.distros import ubuntu from cloudinit.sources import DataSourceScaleway -from tests.unittests.helpers import ( - CiTestCase, - mock, - responses_assert_call_count, -) +from tests.unittests.helpers import mock, responses_assert_call_count class DataResponses: @@ -76,11 +73,7 @@ def get_ok(cls, response): return 200, response.headers, json.dumps(cls.FAKE_METADATA) -class TestOnScaleway(CiTestCase): - def setUp(self): - super(TestOnScaleway, self).setUp() - self.tmp = self.tmp_dir() - +class TestOnScaleway: def install_mocks(self, fake_dmi, fake_file_exists, fake_cmdline): mock, faked = fake_dmi mock.return_value = "Scaleway" if faked else "Whatever" @@ -99,20 +92,20 @@ def install_mocks(self, fake_dmi, fake_file_exists, fake_cmdline): @mock.patch("os.path.exists") @mock.patch("cloudinit.dmi.read_dmi_data") def test_not_ds_detect( - self, m_read_dmi_data, m_file_exists, m_get_cmdline + self, m_read_dmi_data, m_file_exists, m_get_cmdline, paths ): self.install_mocks( fake_dmi=(m_read_dmi_data, False), fake_file_exists=(m_file_exists, False), fake_cmdline=(m_get_cmdline, False), ) - self.assertFalse(DataSourceScaleway.DataSourceScaleway.ds_detect()) + assert False is DataSourceScaleway.DataSourceScaleway.ds_detect() # When not on Scaleway, get_data() returns False. datasource = DataSourceScaleway.DataSourceScaleway( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": self.tmp}) + settings.CFG_BUILTIN, None, paths ) - self.assertFalse(datasource.get_data()) + assert False is datasource.get_data() @mock.patch("cloudinit.util.get_cmdline") @mock.patch("os.path.exists") @@ -129,7 +122,7 @@ def test_ds_detect_dmi( fake_file_exists=(m_file_exists, False), fake_cmdline=(m_get_cmdline, False), ) - self.assertTrue(DataSourceScaleway.DataSourceScaleway.ds_detect()) + assert True is DataSourceScaleway.DataSourceScaleway.ds_detect() @mock.patch("cloudinit.util.get_cmdline") @mock.patch("os.path.exists") @@ -145,7 +138,7 @@ def test_ds_detect_var_run_scaleway( fake_file_exists=(m_file_exists, True), fake_cmdline=(m_get_cmdline, False), ) - self.assertTrue(DataSourceScaleway.DataSourceScaleway.ds_detect()) + assert True is DataSourceScaleway.DataSourceScaleway.ds_detect() @mock.patch("cloudinit.util.get_cmdline") @mock.patch("os.path.exists") @@ -161,7 +154,7 @@ def test_ds_detect_cmdline( fake_file_exists=(m_file_exists, False), fake_cmdline=(m_get_cmdline, True), ) - self.assertTrue(DataSourceScaleway.DataSourceScaleway.ds_detect()) + assert True is DataSourceScaleway.DataSourceScaleway.ds_detect() def get_source_address_adapter(*args, **kwargs): @@ -191,15 +184,14 @@ def _fix_mocking_url(url: str) -> str: ).geturl() -class TestDataSourceScaleway(CiTestCase): - def setUp(self): - tmp = self.tmp_dir() +class TestDataSourceScaleway: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, paths, tmp_path): distro = ubuntu.Distro("", {}, {}) - distro.get_tmp_exec_path = self.tmp_dir + distro.get_tmp_exec_path = str(tmp_path) self.datasource = DataSourceScaleway.DataSourceScaleway( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": tmp}) + settings.CFG_BUILTIN, distro, paths ) - super(TestDataSourceScaleway, self).setUp() self.base_urls = DataSourceScaleway.DS_BASE_URLS for url in self.base_urls: @@ -214,15 +206,12 @@ def setUp(self): ) # Define the metadata URLS - self.add_patch( - "cloudinit.sources.DataSourceScaleway." - "DataSourceScaleway.ds_detect", - "_m_ds_detect", + mocker.patch( + "cloudinit.sources.DataSourceScaleway.DataSourceScaleway.ds_detect", return_value=True, ) - self.add_patch( + mocker.patch( "cloudinit.distros.net.find_fallback_nic", - "_m_find_fallback_nic", return_value="scalewaynic0", ) @@ -231,14 +220,14 @@ def test_set_metadata_url_ipv4_ok(self): self.datasource._set_metadata_url([self.base_urls[0]]) - self.assertTrue(self.base_urls[0] in self.datasource.metadata_url) + assert self.base_urls[0] in self.datasource.metadata_url @responses.activate def test_set_metadata_url_ipv6_ok(self): self.datasource._set_metadata_url([self.base_urls[1]]) - self.assertTrue(self.base_urls[1] in self.datasource.metadata_url) + assert self.base_urls[1] in self.datasource.metadata_url @responses.activate @mock.patch( @@ -286,34 +275,32 @@ def test_ipv4_metadata_ok(self, dhcpv4, ds_detect): f"{self.base_urls[0]}/vendor_data/cloud-init", callback=DataResponses.get_ok, ) - self.assertTrue(self.datasource.get_data()) + assert self.datasource.get_data() - self.assertEqual( - self.datasource.get_instance_id(), - MetadataResponses.FAKE_METADATA["id"], + assert ( + self.datasource.get_instance_id() + == MetadataResponses.FAKE_METADATA["id"] ) ssh_keys = self.datasource.get_public_ssh_keys() ssh_keys.sort() - self.assertEqual( - ssh_keys, - [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", - ], - ) - self.assertEqual( - self.datasource.get_hostname().hostname, - MetadataResponses.FAKE_METADATA["hostname"], + assert ssh_keys == [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", + ] + assert ( + self.datasource.get_hostname().hostname + == MetadataResponses.FAKE_METADATA["hostname"] ) - self.assertEqual( - self.datasource.get_userdata_raw(), DataResponses.FAKE_USER_DATA + assert ( + self.datasource.get_userdata_raw() == DataResponses.FAKE_USER_DATA ) - self.assertEqual( - self.datasource.get_vendordata_raw(), DataResponses.FAKE_USER_DATA + assert ( + self.datasource.get_vendordata_raw() + == DataResponses.FAKE_USER_DATA ) - self.assertIsNone(self.datasource.availability_zone) - self.assertIsNone(self.datasource.region) + assert self.datasource.availability_zone is None + assert self.datasource.region is None @responses.activate @mock.patch( @@ -371,32 +358,30 @@ def test_ipv4_metadata_timeout_ipv6_ok(self, dhcpv4, inet6, ds_detect): f"{self.datasource.metadata_urls[1]}/conf?format=json", 1 ) - self.assertEqual( - self.datasource.get_instance_id(), - MetadataResponses.FAKE_METADATA["id"], + assert ( + self.datasource.get_instance_id() + == MetadataResponses.FAKE_METADATA["id"] ) ssh_keys = self.datasource.get_public_ssh_keys() ssh_keys.sort() - self.assertEqual( - ssh_keys, - [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", - ], - ) - self.assertEqual( - self.datasource.get_hostname().hostname, - MetadataResponses.FAKE_METADATA["hostname"], + assert ssh_keys == [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", + ] + assert ( + self.datasource.get_hostname().hostname + == MetadataResponses.FAKE_METADATA["hostname"] ) - self.assertEqual( - self.datasource.get_userdata_raw(), DataResponses.FAKE_USER_DATA + assert ( + self.datasource.get_userdata_raw() == DataResponses.FAKE_USER_DATA ) - self.assertEqual( - self.datasource.get_vendordata_raw(), DataResponses.FAKE_USER_DATA + assert ( + self.datasource.get_vendordata_raw() + == DataResponses.FAKE_USER_DATA ) - self.assertIsNone(self.datasource.availability_zone) - self.assertIsNone(self.datasource.region) + assert self.datasource.availability_zone is None + assert self.datasource.region is None @responses.activate @mock.patch( @@ -428,14 +413,12 @@ def test_ipv4_ipv6_metadata_timeout(self, dhcpv4, inet6, sleep, ds_detect): callback=ConnectTimeout, ) self.datasource.max_wait = 0 - ret = self.datasource.get_data() + assert False is self.datasource.get_data() responses_assert_call_count(f"{self.datasource.metadata_urls[0]}", 2) responses_assert_call_count(f"{self.datasource.metadata_urls[1]}", 2) - - self.assertFalse(ret) - self.assertEqual(self.datasource.metadata, {}) - self.assertIsNone(self.datasource.get_userdata_raw()) - self.assertIsNone(self.datasource.get_vendordata_raw()) + assert self.datasource.metadata == {} + assert self.datasource.get_userdata_raw() is None + assert self.datasource.get_vendordata_raw() is None @responses.activate @mock.patch( @@ -470,11 +453,9 @@ def test_metadata_ipv4_404(self, dhcpv4, ds_detect): callback=DataResponses.empty, ) self.datasource.get_data() - self.assertEqual( - self.datasource.metadata, MetadataResponses.FAKE_METADATA - ) - self.assertIsNone(self.datasource.get_userdata_raw()) - self.assertIsNone(self.datasource.get_vendordata_raw()) + assert self.datasource.metadata == MetadataResponses.FAKE_METADATA + assert self.datasource.get_userdata_raw() is None + assert self.datasource.get_vendordata_raw() is None @responses.activate @mock.patch("cloudinit.url_helper.time.sleep", lambda x: None) @@ -492,16 +473,16 @@ def test_metadata_connection_errors_legacy_ipv4_url(self, dhcpv4): ] responses.reset() - with self.assertRaises(ConnectionError): + with pytest.raises(ConnectionError): responses.add_callback( responses.GET, f"{self.datasource.metadata_urls[0]}/", callback=ConnectionError, ) self.datasource._set_metadata_url(self.datasource.metadata_urls) - self.assertEqual(self.datasource.metadata, {}) - self.assertIsNone(self.datasource.get_userdata_raw()) - self.assertIsNone(self.datasource.get_vendordata_raw()) + assert self.datasource.metadata == {} + assert self.datasource.get_userdata_raw() is None + assert self.datasource.get_vendordata_raw() is None @responses.activate @mock.patch( @@ -561,8 +542,8 @@ def test_metadata_connection_errors_two_urls( f"{self.datasource.metadata_urls[1]}", 2, ) - self.assertIsNone(self.datasource.get_userdata_raw()) - self.assertIsNone(self.datasource.get_vendordata_raw()) + assert self.datasource.get_userdata_raw() is None + assert self.datasource.get_vendordata_raw() is None @responses.activate @mock.patch( @@ -611,10 +592,10 @@ def _callback(request): callback=_callback, ) self.datasource.get_data() - self.assertEqual( - self.datasource.get_userdata_raw(), DataResponses.FAKE_USER_DATA + assert ( + self.datasource.get_userdata_raw() == DataResponses.FAKE_USER_DATA ) - self.assertEqual(sleep.call_count, 2) + assert sleep.call_count == 2 def test_ssh_keys_empty(self): """ @@ -623,7 +604,7 @@ def test_ssh_keys_empty(self): """ self.datasource.metadata["tags"] = [] self.datasource.metadata["ssh_public_keys"] = [] - self.assertEqual(self.datasource.get_public_ssh_keys(), []) + assert self.datasource.get_public_ssh_keys() == [] def test_ssh_keys_only_tags(self): """ @@ -636,13 +617,10 @@ def test_ssh_keys_only_tags(self): self.datasource.metadata["ssh_public_keys"] = [] ssh_keys = self.datasource.get_public_ssh_keys() ssh_keys.sort() - self.assertEqual( - ssh_keys, - [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", - ], - ) + assert ssh_keys == [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", + ] def test_ssh_keys_only_conf(self): """ @@ -662,13 +640,10 @@ def test_ssh_keys_only_conf(self): ] ssh_keys = self.datasource.get_public_ssh_keys() ssh_keys.sort() - self.assertEqual( - ssh_keys, - [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", - ], - ) + assert ssh_keys == [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", + ] def test_ssh_keys_both(self): """ @@ -691,14 +666,11 @@ def test_ssh_keys_both(self): ] ssh_keys = self.datasource.get_public_ssh_keys() ssh_keys.sort() - self.assertEqual( - ssh_keys, - [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", - ], - ) + assert ssh_keys == [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD", + ] @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -723,7 +695,7 @@ def test_legacy_network_config_ok(self, m_get_cmdline, fallback_nic): } ], } - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -766,7 +738,7 @@ def test_legacy_network_config_ipv6_ok(self, m_get_cmdline, fallback_nic): } ], } - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -779,7 +751,7 @@ def test_legacy_network_config_existing(self, m_get_cmdline, fallback_nic): self.datasource._network_config = "0xdeadbeef" netcfg = self.datasource.network_config - self.assertEqual(netcfg, "0xdeadbeef") + assert netcfg == "0xdeadbeef" @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -806,7 +778,7 @@ def test_legacy_network_config_unset(self, m_get_cmdline, fallback_nic): } netcfg = self.datasource.network_config - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.sources.DataSourceScaleway.LOG.warning") @mock.patch("cloudinit.distros.net.find_fallback_nic") @@ -836,7 +808,7 @@ def test_legacy_network_config_cached_none( } netcfg = self.datasource.network_config - self.assertEqual(netcfg, resp) + assert netcfg == resp logwarning.assert_called_with( "Found None as cached _network_config. Resetting to %s", sources.UNSET, @@ -873,7 +845,7 @@ def test_ipmob_primary_ipv4_config_ok(self, m_get_cmdline, fallback_nic): }, } - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -919,7 +891,7 @@ def test_ipmob_additional_ipv4_config_ok( }, }, } - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -957,7 +929,7 @@ def test_ipmob_primary_ipv6_config_ok(self, m_get_cmdline, fallback_nic): }, } - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -1008,7 +980,7 @@ def test_ipmob_primary_ipv4_v6_config_ok( }, } - self.assertEqual(netcfg, resp) + assert netcfg == resp @mock.patch("cloudinit.distros.net.find_fallback_nic") @mock.patch("cloudinit.util.get_cmdline") @@ -1059,4 +1031,4 @@ def test_ipmob_primary_ipv6_v4_config_ok( }, } - self.assertEqual(netcfg, resp) + assert netcfg == resp diff --git a/tests/unittests/sources/test_upcloud.py b/tests/unittests/sources/test_upcloud.py index a6a805e091f..85f5144e1c3 100644 --- a/tests/unittests/sources/test_upcloud.py +++ b/tests/unittests/sources/test_upcloud.py @@ -3,15 +3,16 @@ # This file is part of cloud-init. See LICENSE file for license information. import json +from unittest import mock import pytest -from cloudinit import helpers, importer, settings, sources +from cloudinit import importer, settings, sources from cloudinit.sources.DataSourceUpCloud import ( DataSourceUpCloud, DataSourceUpCloudLocal, ) -from tests.unittests.helpers import CiTestCase, example_netdev, mock +from tests.unittests.helpers import example_netdev UC_METADATA = json.loads( """ @@ -149,73 +150,50 @@ def _mock_dmi(): return True, "00322b68-0096-4042-9406-faad61922128" -class TestUpCloudMetadata(CiTestCase): +class TestUpCloudMetadata: """ Test reading the meta-data """ - def setUp(self): - super(TestUpCloudMetadata, self).setUp() - self.tmp = self.tmp_dir() - - def get_ds(self, get_sysinfo=_mock_dmi): - ds = DataSourceUpCloud( - settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": self.tmp}) - ) - if get_sysinfo: - ds._get_sysinfo = get_sysinfo - return ds - @mock.patch("cloudinit.sources.helpers.upcloud.read_sysinfo") - def test_returns_false_not_on_upcloud(self, m_read_sysinfo): + def test_returns_false_not_on_upcloud(self, m_read_sysinfo, paths): m_read_sysinfo.return_value = (False, None) - ds = self.get_ds(get_sysinfo=None) - self.assertEqual(False, ds.get_data()) - self.assertTrue(m_read_sysinfo.called) + ds = DataSourceUpCloud(settings.CFG_BUILTIN, None, paths) + assert False is ds.get_data() + assert m_read_sysinfo.called @mock.patch("cloudinit.sources.helpers.upcloud.read_metadata") - def test_metadata(self, mock_readmd): + def test_metadata(self, mock_readmd, paths): mock_readmd.return_value = UC_METADATA.copy() - ds = self.get_ds() + ds = DataSourceUpCloud(settings.CFG_BUILTIN, None, paths) + ds._get_sysinfo = _mock_dmi ds.perform_dhcp_setup = False - ret = ds.get_data() - self.assertTrue(ret) + assert ds.get_data() is True + assert mock_readmd.called - self.assertTrue(mock_readmd.called) + assert UC_METADATA.get("user_data") == ds.get_userdata_raw() + assert UC_METADATA.get("vendor_data") == ds.get_vendordata_raw() + assert UC_METADATA.get("region") == ds.availability_zone + assert UC_METADATA.get("instance_id") == ds.get_instance_id() + assert UC_METADATA.get("cloud_name") == ds.cloud_name - self.assertEqual(UC_METADATA.get("user_data"), ds.get_userdata_raw()) - self.assertEqual( - UC_METADATA.get("vendor_data"), ds.get_vendordata_raw() - ) - self.assertEqual(UC_METADATA.get("region"), ds.availability_zone) - self.assertEqual(UC_METADATA.get("instance_id"), ds.get_instance_id()) - self.assertEqual(UC_METADATA.get("cloud_name"), ds.cloud_name) - - self.assertEqual( - UC_METADATA.get("public_keys"), ds.get_public_ssh_keys() - ) - self.assertIsInstance(ds.get_public_ssh_keys(), list) + assert UC_METADATA.get("public_keys") == ds.get_public_ssh_keys() + assert isinstance(ds.get_public_ssh_keys(), list) -class TestUpCloudNetworkSetup(CiTestCase): +class TestUpCloudNetworkSetup: """ Test reading the meta-data on networked context """ - def setUp(self): - super(TestUpCloudNetworkSetup, self).setUp() - self.tmp = self.tmp_dir() - - def get_ds(self, get_sysinfo=_mock_dmi): + @pytest.fixture + def ds(self, paths, tmp_path): distro = mock.MagicMock() - distro.get_tmp_exec_path = self.tmp_dir - ds = DataSourceUpCloudLocal( - settings.CFG_BUILTIN, distro, helpers.Paths({"run_dir": self.tmp}) - ) - if get_sysinfo: - ds._get_sysinfo = get_sysinfo + distro.get_tmp_exec_path = str(tmp_path) + ds = DataSourceUpCloudLocal(settings.CFG_BUILTIN, distro, paths) + ds._get_sysinfo = _mock_dmi return ds @pytest.mark.usefixtures("disable_netdev_info") @@ -224,7 +202,7 @@ def get_ds(self, get_sysinfo=_mock_dmi): @mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery") @mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network") def test_network_configured_metadata( - self, m_net, m_dhcp, m_fallback_nic, mock_readmd + self, m_net, m_dhcp, m_fallback_nic, mock_readmd, ds ): mock_readmd.return_value = UC_METADATA.copy() @@ -237,12 +215,9 @@ def test_network_configured_metadata( "broadcast-address": "10.6.3.255", } - ds = self.get_ds() - - ret = ds.get_data() - self.assertTrue(ret) + assert ds.get_data() is True - self.assertTrue(m_dhcp.called) + assert m_dhcp.called m_dhcp.assert_called_with(ds.distro, "eth1", None) m_net.assert_called_once_with( @@ -256,19 +231,19 @@ def test_network_configured_metadata( static_routes=None, ) - self.assertTrue(mock_readmd.called) + assert mock_readmd.called - self.assertEqual(UC_METADATA.get("region"), ds.availability_zone) - self.assertEqual(UC_METADATA.get("instance_id"), ds.get_instance_id()) - self.assertEqual(UC_METADATA.get("cloud_name"), ds.cloud_name) + assert UC_METADATA.get("region") == ds.availability_zone + assert UC_METADATA.get("instance_id") == ds.get_instance_id() + assert UC_METADATA.get("cloud_name") == ds.cloud_name @mock.patch("cloudinit.sources.helpers.upcloud.read_metadata") @mock.patch("cloudinit.net.get_interfaces_by_mac") - def test_network_configuration(self, m_get_by_mac, mock_readmd): + def test_network_configuration(self, m_get_by_mac, mock_readmd, ds): mock_readmd.return_value = UC_METADATA.copy() raw_ifaces = UC_METADATA.get("network").get("interfaces") - self.assertEqual(4, len(raw_ifaces)) + assert 4 == len(raw_ifaces) m_get_by_mac.return_value = { raw_ifaces[0].get("mac"): "eth0", @@ -277,52 +252,47 @@ def test_network_configuration(self, m_get_by_mac, mock_readmd): raw_ifaces[3].get("mac"): "eth3", } - ds = self.get_ds() ds.perform_dhcp_setup = False - ret = ds.get_data() - self.assertTrue(ret) - - self.assertTrue(mock_readmd.called) + assert ds.get_data() is True + assert mock_readmd.called netcfg = ds.network_config - self.assertEqual(1, netcfg.get("version")) + assert 1 == netcfg.get("version") config = netcfg.get("config") - self.assertIsInstance(config, list) - self.assertEqual(5, len(config)) - self.assertEqual("physical", config[3].get("type")) - - self.assertEqual( - raw_ifaces[2].get("mac"), config[2].get("mac_address") - ) - self.assertEqual(1, len(config[2].get("subnets"))) - self.assertEqual( - "ipv6_dhcpv6-stateless", config[2].get("subnets")[0].get("type") + assert isinstance(config, list) + assert 5 == len(config) + assert "physical" == config[3].get("type") + + assert raw_ifaces[2].get("mac") == config[2].get("mac_address") + assert 1 == len(config[2].get("subnets")) + assert "ipv6_dhcpv6-stateless" == config[2].get("subnets")[0].get( + "type" ) - self.assertEqual(2, len(config[0].get("subnets"))) - self.assertEqual("static", config[0].get("subnets")[1].get("type")) + assert 2 == len(config[0].get("subnets")) + assert "static" == config[0].get("subnets")[1].get("type") dns = config[4] - self.assertEqual("nameserver", dns.get("type")) - self.assertEqual(2, len(dns.get("address"))) - self.assertEqual( - UC_METADATA.get("network").get("dns")[1], dns.get("address")[1] + assert "nameserver" == dns.get("type") + assert 2 == len(dns.get("address")) + assert ( + UC_METADATA.get("network").get("dns")[1] == dns.get("address")[1] ) -class TestUpCloudDatasourceLoading(CiTestCase): +class TestUpCloudDatasourceLoading: def test_get_datasource_list_returns_in_local(self): deps = (sources.DEP_FILESYSTEM,) ds_list = sources.DataSourceUpCloud.get_datasource_list(deps) - self.assertEqual(ds_list, [DataSourceUpCloudLocal]) + assert ds_list == [DataSourceUpCloudLocal] def test_get_datasource_list_returns_in_normal(self): deps = (sources.DEP_FILESYSTEM, sources.DEP_NETWORK) ds_list = sources.DataSourceUpCloud.get_datasource_list(deps) - self.assertEqual(ds_list, [DataSourceUpCloud]) + assert ds_list == [DataSourceUpCloud] @mock.patch.object( importer, @@ -335,4 +305,4 @@ def test_list_sources_finds_ds(self): (sources.DEP_FILESYSTEM, sources.DEP_NETWORK), ["cloudinit.sources"], ) - self.assertEqual([DataSourceUpCloud], found) + assert [DataSourceUpCloud] == found From 52ec5ae241aa40cab98d79a2eafecef7090bf99e Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 14 Aug 2025 23:35:36 +0200 Subject: [PATCH 076/150] test: drop citestcase from s* tests (#6378) --- tests/unittests/test_simpletable.py | 11 +-- tests/unittests/test_subp.py | 147 +++++++++++++--------------- 2 files changed, 74 insertions(+), 84 deletions(-) diff --git a/tests/unittests/test_simpletable.py b/tests/unittests/test_simpletable.py index ee7eb0b48d8..f5d12f20397 100644 --- a/tests/unittests/test_simpletable.py +++ b/tests/unittests/test_simpletable.py @@ -10,7 +10,6 @@ """ from cloudinit.simpletable import SimpleTable -from tests.unittests.helpers import CiTestCase # Examples rendered by cloud-init using PrettyTable NET_DEVICE_FIELDS = ("Device", "Up", "Address", "Mask", "Scope", "Hw-Address") @@ -85,25 +84,25 @@ +--------+""" -class TestSimpleTable(CiTestCase): +class TestSimpleTable: def test_no_rows(self): """An empty table is rendered as PrettyTable would have done it.""" table = SimpleTable(NO_ROWS_FIELDS) - self.assertEqual(str(table), NO_ROWS_TABLE) + assert str(table) == NO_ROWS_TABLE def test_net_dev(self): """Net device info is rendered as it was with PrettyTable.""" table = SimpleTable(NET_DEVICE_FIELDS) for row in NET_DEVICE_ROWS: table.add_row(row) - self.assertEqual(str(table), NET_DEVICE_TABLE) + assert str(table) == NET_DEVICE_TABLE def test_route_ipv4(self): """Route IPv4 info is rendered as it was with PrettyTable.""" table = SimpleTable(ROUTE_IPV4_FIELDS) for row in ROUTE_IPV4_ROWS: table.add_row(row) - self.assertEqual(str(table), ROUTE_IPV4_TABLE) + assert str(table) == ROUTE_IPV4_TABLE def test_authorized_keys(self): """SSH authorized keys are rendered as they were with PrettyTable.""" @@ -116,4 +115,4 @@ def test_get_string(self): table = SimpleTable(AUTHORIZED_KEYS_FIELDS) for row in AUTHORIZED_KEYS_ROWS: table.add_row(row) - self.assertEqual(table.get_string(), str(table)) + assert table.get_string() == str(table) diff --git a/tests/unittests/test_subp.py b/tests/unittests/test_subp.py index eca48800cb3..27921955efb 100644 --- a/tests/unittests/test_subp.py +++ b/tests/unittests/test_subp.py @@ -3,36 +3,38 @@ """Tests for cloudinit.subp utility functions""" import json +import logging import os import stat import sys from unittest import mock +import pytest + from cloudinit import subp, util from tests.helpers import get_top_level_dir -from tests.unittests.helpers import CiTestCase SH = "sh" BOGUS_COMMAND = "this-is-not-expected-to-be-a-program-name" -class TestPrependBaseCommands(CiTestCase): - with_logs = True +class TestPrependBaseCommands: def test_prepend_base_command_errors_on_neither_string_nor_list(self): """Raise an error for each command which is not a string or list.""" orig_commands = ["ls", 1, {"not": "gonna work"}, ["basecmd", "list"]] - with self.assertRaises(TypeError) as context_manager: + with pytest.raises( + TypeError, + match="Invalid basecmd config. These commands are not a string or" + " list:\n1\n{'not': 'gonna work'}", + ): subp.prepend_base_command( base_command="basecmd", commands=orig_commands ) - self.assertEqual( - "Invalid basecmd config. These commands are not a string or" - " list:\n1\n{'not': 'gonna work'}", - str(context_manager.exception), - ) - def test_prepend_base_command_warns_on_non_base_string_commands(self): + def test_prepend_base_command_warns_on_non_base_string_commands( + self, caplog + ): """Warn on each non-base for commands of type string.""" orig_commands = [ "ls", @@ -43,14 +45,16 @@ def test_prepend_base_command_warns_on_non_base_string_commands(self): fixed_commands = subp.prepend_base_command( base_command="basecmd", commands=orig_commands ) - self.assertEqual( - "WARNING: Non-basecmd commands in basecmd config:\n" - "ls\ntouch /blah\n", - self.logs.getvalue(), - ) - self.assertEqual(orig_commands, fixed_commands) - - def test_prepend_base_command_prepends_on_non_base_list_commands(self): + assert ( + mock.ANY, + logging.WARNING, + "Non-basecmd commands in basecmd config:\nls\ntouch /blah", + ) in caplog.record_tuples + assert orig_commands == fixed_commands + + def test_prepend_base_command_prepends_on_non_base_list_commands( + self, caplog + ): """Prepend 'basecmd' for each non-basecmd command of type list.""" orig_commands = [ ["ls"], @@ -67,10 +71,10 @@ def test_prepend_base_command_prepends_on_non_base_list_commands(self): fixed_commands = subp.prepend_base_command( base_command="basecmd", commands=orig_commands ) - self.assertEqual("", self.logs.getvalue()) - self.assertEqual(expected, fixed_commands) + assert "" == caplog.text + assert expected == fixed_commands - def test_prepend_base_command_removes_first_item_when_none(self): + def test_prepend_base_command_removes_first_item_when_none(self, caplog): """Remove the first element of a non-basecmd when it is None.""" orig_commands = [ [None, "ls"], @@ -87,20 +91,12 @@ def test_prepend_base_command_removes_first_item_when_none(self): fixed_commands = subp.prepend_base_command( base_command="basecmd", commands=orig_commands ) - self.assertEqual("", self.logs.getvalue()) - self.assertEqual(expected, fixed_commands) - + assert "" == caplog.text + assert expected == fixed_commands -class TestSubp(CiTestCase): - allowed_subp = [ - SH, - "cat", - CiTestCase.SUBP_SHELL_TRUE, - BOGUS_COMMAND, - sys.executable, - "env", - ] +@pytest.mark.allow_all_subp +class TestSubp: stdin2err = [SH, "-c", "cat >&2"] stdin2out = ["cat"] utf8_invalid = b"ab\xaadef" @@ -112,38 +108,38 @@ def printf_cmd(arg): """print with builtin printf""" return [SH, "-c", 'printf "$@"', "printf", arg] - def test_subp_handles_bytestrings(self): + def test_subp_handles_bytestrings(self, tmp_path): """subp can run a bytestring command if shell is True.""" - tmp_file = self.tmp_path("test.out") + tmp_file = str(tmp_path / "test.out") cmd = "echo HI MOM >> {tmp_file}".format(tmp_file=tmp_file) (out, _err) = subp.subp(cmd.encode("utf-8"), shell=True) - self.assertEqual("", out) - self.assertEqual("", _err) - self.assertEqual("HI MOM\n", util.load_text_file(tmp_file)) + assert "" == out + assert "" == _err + assert "HI MOM\n" == util.load_text_file(tmp_file) - def test_subp_handles_strings(self): + def test_subp_handles_strings(self, tmp_path): """subp can run a string command if shell is True.""" - tmp_file = self.tmp_path("test.out") + tmp_file = str(tmp_path / "test.out") cmd = "echo HI MOM >> {tmp_file}".format(tmp_file=tmp_file) (out, _err) = subp.subp(cmd, shell=True) - self.assertEqual("", out) - self.assertEqual("", _err) - self.assertEqual("HI MOM\n", util.load_text_file(tmp_file)) + assert "" == out + assert "" == _err + assert "HI MOM\n" == util.load_text_file(tmp_file) def test_subp_handles_utf8(self): # The given bytes contain utf-8 accented characters as seen in e.g. # the "deja dup" package in Ubuntu. cmd = self.printf_cmd(self.utf8_valid_2) (out, _err) = subp.subp(cmd, capture=True) - self.assertEqual(out, self.utf8_valid_2.decode("utf-8")) + assert out == self.utf8_valid_2.decode("utf-8") def test_subp_respects_decode_false(self): (out, err) = subp.subp( self.stdin2out, capture=True, decode=False, data=self.utf8_valid ) - self.assertTrue(isinstance(out, bytes)) - self.assertTrue(isinstance(err, bytes)) - self.assertEqual(out, self.utf8_valid) + assert isinstance(out, bytes) + assert isinstance(err, bytes) + assert out == self.utf8_valid def test_subp_decode_ignore(self): """ensure that invalid utf-8 is ignored with the "ignore" kwarg""" @@ -166,14 +162,14 @@ def test_subp_decode_strict_valid_utf8(self): (out, _err) = subp.subp( self.stdin2out, capture=True, decode="strict", data=self.utf8_valid ) - self.assertEqual(out, self.utf8_valid.decode("utf-8")) + assert out == self.utf8_valid.decode("utf-8") def test_subp_decode_invalid_utf8_replaces(self): (out, _err) = subp.subp( self.stdin2out, capture=True, data=self.utf8_invalid ) expected = self.utf8_invalid.decode("utf-8", "replace") - self.assertEqual(out, expected) + assert out == expected def test_subp_decode_strict_raises(self): args = [] @@ -183,7 +179,8 @@ def test_subp_decode_strict_raises(self): "decode": "strict", "data": self.utf8_invalid, } - self.assertRaises(UnicodeDecodeError, subp.subp, *args, **kwargs) + with pytest.raises(UnicodeDecodeError): + subp.subp(*args, **kwargs) def test_subp_capture_stderr(self): data = b"hello world" @@ -194,8 +191,8 @@ def test_subp_capture_stderr(self): data=data, update_env={"LC_ALL": "C"}, ) - self.assertEqual(err, data) - self.assertEqual(out, b"") + assert err == data + assert out == b"" def test_subp_reads_env(self): with mock.patch.dict("os.environ", values={"FOO": "BAR"}): @@ -217,54 +214,48 @@ def test_subp_update_env(self): set(out.splitlines()) ) - def test_subp_warn_missing_shebang(self): + def test_subp_warn_missing_shebang(self, tmp_path): """Warn on no #! in script""" - noshebang = self.tmp_path("noshebang") + noshebang = str(tmp_path / "noshebang") util.write_file(noshebang, "true\n") print("os is %s" % os) os.chmod(noshebang, os.stat(noshebang).st_mode | stat.S_IEXEC) - with self.allow_subp([noshebang]): - self.assertRaisesRegex( - subp.ProcessExecutionError, - r"Missing #! in script\?", - subp.subp, + with pytest.raises( + subp.ProcessExecutionError, match=r"Missing #! in script\?" + ): + subp.subp( (noshebang,), ) def test_returns_none_if_no_capture(self): (out, err) = subp.subp(self.stdin2out, data=b"", capture=False) - self.assertIsNone(err) - self.assertIsNone(out) + assert err is None + assert out is None def test_exception_has_out_err_are_bytes_if_decode_false(self): """Raised exc should have stderr, stdout as bytes if no decode.""" - with self.assertRaises(subp.ProcessExecutionError) as cm: + with pytest.raises(subp.ProcessExecutionError) as exc_info: subp.subp([BOGUS_COMMAND], decode=False) - self.assertTrue(isinstance(cm.exception.stdout, bytes)) - self.assertTrue(isinstance(cm.exception.stderr, bytes)) + assert isinstance(exc_info.value.stdout, bytes) + assert isinstance(exc_info.value.stderr, bytes) def test_exception_has_out_err_are_bytes_if_decode_true(self): """Raised exc should have stderr, stdout as string if no decode.""" - with self.assertRaises(subp.ProcessExecutionError) as cm: + with pytest.raises(subp.ProcessExecutionError) as exc_info: subp.subp([BOGUS_COMMAND], decode=True) - self.assertTrue(isinstance(cm.exception.stdout, str)) - self.assertTrue(isinstance(cm.exception.stderr, str)) + assert isinstance(exc_info.value.stdout, str) + assert isinstance(exc_info.value.stderr, str) def test_exception_invalid_command(self): args = [None, "first", "arg", "missing"] - with self.assertRaises( - subp.ProcessExecutionError, msg="Running invalid command" - ): - with self.allow_subp(args): - subp.subp(args) + with pytest.raises(subp.ProcessExecutionError): + subp.subp(args) def test_bunch_of_slashes_in_path(self): - self.assertEqual( - "/target/my/path/", subp.target_path("/target/", "//my/path/") - ) - self.assertEqual( - "/target/my/path/", subp.target_path("/target/", "///my/path/") + assert "/target/my/path/" == subp.target_path("/target/", "//my/path/") + assert "/target/my/path/" == subp.target_path( + "/target/", "///my/path/" ) def test_c_lang_can_take_utf8_args(self): @@ -308,4 +299,4 @@ def test_c_lang_can_take_utf8_args(self): data=json.dumps(cmd).encode("utf-8"), decode=False, ) - self.assertEqual(self.utf8_valid, out) + assert self.utf8_valid == out From d2f422d17493c8cef443c0262184d93c900f716a Mon Sep 17 00:00:00 2001 From: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com> Date: Fri, 15 Aug 2025 04:09:09 +0530 Subject: [PATCH 077/150] Add 'to: default' support in network state (#6391) Add `to: default` support during routes configuration from network config v2 Signed-off-by: Shreenidhi Shedi --- cloudinit/net/network_state.py | 17 +++++++++++++++ .../reference/network-config-format-v2.rst | 7 +++++-- .../net/artifacts/photon_net_config_v2.yaml | 4 ++-- tests/unittests/net/test_network_state.py | 21 +++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index 0d250ecdcf6..ab7bb2c9a50 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -16,6 +16,7 @@ ipv4_mask_to_net_prefix, ipv6_mask_to_net_prefix, is_ip_network, + is_ipv4_address, is_ipv4_network, is_ipv6_address, is_ipv6_network, @@ -991,6 +992,22 @@ def _normalize_net_keys(network, address_keys=()): raise ValueError(message) addr = str(net.get(addr_key)) + if addr == "default": + gw_ip = str(net.get("gateway")) + if not gw_ip: + message = "Gateway IP is empty" + LOG.error(message) + raise ValueError(message) + + if is_ipv4_address(gw_ip): + addr = "0.0.0.0/0" + elif is_ipv6_address(gw_ip): + addr = "::/0" + else: + message = f"Invalid Gateway IP: '{gw_ip}'" + LOG.error(message) + raise ValueError(message) + if not is_ip_network(addr): LOG.error("Address %s is not a valid ip network", addr) raise ValueError(f"Address {addr} is not a valid ip address") diff --git a/doc/rtd/reference/network-config-format-v2.rst b/doc/rtd/reference/network-config-format-v2.rst index c90e3b62a86..b26ab6e6375 100644 --- a/doc/rtd/reference/network-config-format-v2.rst +++ b/doc/rtd/reference/network-config-format-v2.rst @@ -296,12 +296,15 @@ Example: :: ----------------------------------- Add device specific routes. Each mapping includes a ``to``, ``via`` key -with an IPv4 or IPv6 address as value. ``metric`` is an optional value. +with an IPv4 or IPv6 address as value. ``to: default`` may be used to +configure the default route. ``metric`` is an optional value. +``table`` is an optional numeric ID or name of the routing table for +policy-based routing. Example: :: routes: - - to: 0.0.0.0/0 + - to: default # could be 0.0.0.0/0 optionally via: 10.23.2.1 metric: 3 diff --git a/tests/unittests/net/artifacts/photon_net_config_v2.yaml b/tests/unittests/net/artifacts/photon_net_config_v2.yaml index 7701158edde..c8d6e4351f8 100644 --- a/tests/unittests/net/artifacts/photon_net_config_v2.yaml +++ b/tests/unittests/net/artifacts/photon_net_config_v2.yaml @@ -10,11 +10,11 @@ network: mtu: 1500 addresses: [192.0.2.10/24] routes: - - to: 0.0.0.0/0 + - to: default via: 192.168.14.1 metric: 50 table: 10 - - to: ::/0 + - to: default via: 2001:1::2 metric: 100 routing-policy: diff --git a/tests/unittests/net/test_network_state.py b/tests/unittests/net/test_network_state.py index 5161b9cce8b..e54c04e86ab 100644 --- a/tests/unittests/net/test_network_state.py +++ b/tests/unittests/net/test_network_state.py @@ -51,9 +51,19 @@ eth0: match: macaddress: '00:11:22:33:44:55' + addresses: + - 192.168.14.10/24 + - 2001:1::100/64 nameservers: search: [spam.local, eggs.local] addresses: [8.8.8.8] + routes: + - to: default + via: 192.168.14.1 + metric: 50 + - to: default + via: 2001:1::2 + metric: 100 eth1: match: macaddress: '66:77:88:99:00:11' @@ -282,6 +292,17 @@ def test_v2_nameservers(self, mocker): # Ensure DNS defined on interface exists on interface for iface in config.iter_interfaces(): if iface["name"] == "eth0": + for route in iface["subnets"][0]["routes"]: + if route["gateway"] == "192.168.14.1": + assert route["network"] == "0.0.0.0" + assert route["prefix"] == 0 + assert route["netmask"] == "0.0.0.0" + elif route["gateway"] == "2001:1::2": + assert route["network"] == "::" + assert route["prefix"] == 0 + assert "netmask" not in route + else: + assert False assert iface["dns"] == { "nameservers": ["8.8.8.8"], "search": ["spam.local", "eggs.local"], From 6a4075f35ebc69a63d9a55ae648ca1e2810ef7a8 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Fri, 15 Aug 2025 00:47:02 +0200 Subject: [PATCH 078/150] test: drop citestcase from test_util.py (#6387) --- tests/unittests/test_util.py | 671 ++++++++++++++++------------------- 1 file changed, 305 insertions(+), 366 deletions(-) diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index 3d681939d14..0a6add54cc4 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -36,7 +36,7 @@ from cloudinit.sources import DataSourceHostname from cloudinit.subp import SubpResult from tests.unittests import helpers -from tests.unittests.helpers import CiTestCase, skipIf, skipUnlessJinja +from tests.unittests.helpers import random_string, skipIf, skipUnlessJinja LOG = logging.getLogger(__name__) M_PATH = "cloudinit.util." @@ -638,56 +638,52 @@ def test_fetch_ssl_details( assert 2 == m_isdir.call_count == m_isfile.call_count -class TestSymlink(CiTestCase): - def test_sym_link_simple(self): - tmpd = self.tmp_dir() - link = self.tmp_path("link", tmpd) - target = self.tmp_path("target", tmpd) +class TestSymlink: + def test_sym_link_simple(self, tmp_path): + link = str(tmp_path / "link") + target = str(tmp_path / "target") util.write_file(target, "hello") util.sym_link(target, link) - self.assertTrue(os.path.exists(link)) - self.assertTrue(os.path.islink(link)) - - def test_sym_link_source_exists(self): - tmpd = self.tmp_dir() - link = self.tmp_path("link", tmpd) - target = self.tmp_path("target", tmpd) - target2 = self.tmp_path("target2", tmpd) + assert os.path.exists(link) + assert os.path.islink(link) + + def test_sym_link_source_exists(self, tmp_path): + link = str(tmp_path / "link") + target = str(tmp_path / "target") + target2 = str(tmp_path / "target2") util.write_file(target, "hello") util.write_file(target2, "hello2") util.sym_link(target, link) - self.assertTrue(os.path.exists(link)) + assert os.path.exists(link) util.sym_link(target2, link, force=True) - self.assertTrue(os.path.exists(link)) - self.assertEqual("hello2", util.load_text_file(link)) + assert os.path.exists(link) + assert "hello2" == util.load_text_file(link) - def test_sym_link_dangling_link(self): - tmpd = self.tmp_dir() - link = self.tmp_path("link", tmpd) - target = self.tmp_path("target", tmpd) + def test_sym_link_dangling_link(self, tmp_path): + link = str(tmp_path / "link") + target = str(tmp_path / "target") util.sym_link(target, link) - self.assertTrue(os.path.islink(link)) - self.assertFalse(os.path.exists(link)) + assert os.path.islink(link) + assert not os.path.exists(link) util.sym_link(target, link, force=True) - self.assertTrue(os.path.islink(link)) - self.assertFalse(os.path.exists(link)) + assert os.path.islink(link) + assert not os.path.exists(link) - def test_sym_link_create_dangling(self): - tmpd = self.tmp_dir() - link = self.tmp_path("link", tmpd) - target = self.tmp_path("target", tmpd) + def test_sym_link_create_dangling(self, tmp_path): + link = str(tmp_path / "link") + target = str(tmp_path / "target") util.sym_link(target, link) - self.assertTrue(os.path.islink(link)) - self.assertFalse(os.path.exists(link)) + assert os.path.islink(link) + assert not os.path.exists(link) -class TestUptime(CiTestCase): +class TestUptime: @mock.patch(M_PATH + "boottime") @mock.patch(M_PATH + "os.path.exists") @mock.patch(M_PATH + "time.time") @@ -698,75 +694,67 @@ def test_uptime_non_linux_path(self, m_time, m_exists, m_boottime): m_time.return_value = boottime + uptime m_exists.return_value = False result = util.uptime() - self.assertEqual(str(uptime), result) + assert str(uptime) == result -class TestShellify(CiTestCase): +class TestShellify: def test_input_dict_raises_type_error(self): - self.assertRaisesRegex( - TypeError, - "Input.*was.*dict.*xpected", - util.shellify, - {"mykey": "myval"}, - ) + with pytest.raises(TypeError, match="Input.*was.*dict.*xpected"): + util.shellify( + {"mykey": "myval"}, + ) def test_input_str_raises_type_error(self): - self.assertRaisesRegex( - TypeError, "Input.*was.*str.*xpected", util.shellify, "foobar" - ) + with pytest.raises(TypeError, match="Input.*was.*str.*xpected"): + util.shellify("foobar") def test_value_with_int_raises_type_error(self): - self.assertRaisesRegex( - TypeError, "shellify.*int", util.shellify, ["foo", 1] - ) + with pytest.raises(TypeError, match="shellify.*int"): + util.shellify(["foo", 1]) def test_supports_strings_and_lists(self): - self.assertEqual( - "\n".join( - [ - "#!/bin/sh", - "echo hi mom", - "'echo' 'hi dad'", - "'echo' 'hi' 'sis'", - "", - ] - ), - util.shellify( - ["echo hi mom", ["echo", "hi dad"], ("echo", "hi", "sis")] - ), + assert "\n".join( + [ + "#!/bin/sh", + "echo hi mom", + "'echo' 'hi dad'", + "'echo' 'hi' 'sis'", + "", + ] + ) == util.shellify( + ["echo hi mom", ["echo", "hi dad"], ("echo", "hi", "sis")] ) def test_supports_comments(self): - self.assertEqual( - "\n".join(["#!/bin/sh", "echo start", "echo end", ""]), - util.shellify(["echo start", None, "echo end"]), - ) + assert "\n".join( + ["#!/bin/sh", "echo start", "echo end", ""] + ) == util.shellify(["echo start", None, "echo end"]) -class TestGetHostnameFqdn(CiTestCase): +class TestGetHostnameFqdn: def test_get_hostname_fqdn_from_only_cfg_fqdn(self): """When cfg only has the fqdn key, derive hostname and fqdn from it.""" hostname, fqdn, _ = util.get_hostname_fqdn( cfg={"fqdn": "myhost.domain.com"}, cloud=None ) - self.assertEqual("myhost", hostname) - self.assertEqual("myhost.domain.com", fqdn) + assert "myhost" == hostname + assert "myhost.domain.com" == fqdn def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self): """When cfg has both fqdn and hostname keys, return them.""" hostname, fqdn, _ = util.get_hostname_fqdn( cfg={"fqdn": "myhost.domain.com", "hostname": "other"}, cloud=None ) - self.assertEqual("other", hostname) - self.assertEqual("myhost.domain.com", fqdn) + assert "other" == hostname + assert "myhost.domain.com" == fqdn def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self): """When cfg has only hostname key which represents a fqdn, use that.""" hostname, fqdn, _ = util.get_hostname_fqdn( cfg={"hostname": "myhost.domain.com"}, cloud=None ) - self.assertEqual("myhost", hostname) - self.assertEqual("myhost.domain.com", fqdn) + assert "myhost" == hostname + assert "myhost.domain.com" == fqdn def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): """When cfg has a hostname without a '.' query cloud.get_hostname.""" @@ -777,8 +765,8 @@ def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): hostname, fqdn, _ = util.get_hostname_fqdn( cfg={"hostname": "myhost"}, cloud=cloud ) - self.assertEqual("myhost", hostname) - self.assertEqual("cloudhost.mycloud.com", fqdn) + assert "myhost" == hostname + assert "cloudhost.mycloud.com" == fqdn assert [ mock.call(fqdn=True, metadata_only=False) ] == cloud.get_hostname.call_args_list @@ -791,8 +779,8 @@ def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self): DataSourceHostname("cloudhost", False), ) hostname, fqdn, _ = util.get_hostname_fqdn(cfg={}, cloud=cloud) - self.assertEqual("cloudhost", hostname) - self.assertEqual("cloudhost.mycloud.com", fqdn) + assert "cloudhost" == hostname + assert "cloudhost.mycloud.com" == fqdn assert [ mock.call(fqdn=True, metadata_only=False), mock.call(metadata_only=False), @@ -803,16 +791,16 @@ def test_get_hostname_fqdn_from_numeric_fqdn(self): hostname, fqdn, _ = util.get_hostname_fqdn( cfg={"fqdn": 12345}, cloud=None ) - self.assertEqual("12345", hostname) - self.assertEqual("12345", fqdn) + assert "12345" == hostname + assert "12345" == fqdn def test_get_hostname_fqdn_from_numeric_fqdn_with_domain(self): """When cfg fqdn is numeric with a domain, ensure correct parsing.""" hostname, fqdn, _ = util.get_hostname_fqdn( cfg={"fqdn": "12345.example.com"}, cloud=None ) - self.assertEqual("12345", hostname) - self.assertEqual("12345.example.com", fqdn) + assert "12345" == hostname + assert "12345.example.com" == fqdn def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): """Calls to cloud.get_hostname pass the metadata_only parameter.""" @@ -830,7 +818,7 @@ def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): ] == cloud.get_hostname.call_args_list -class TestBlkid(CiTestCase): +class TestBlkid: ids = { "id01": "1111-1111", "id02": "22222222-2222", @@ -858,8 +846,6 @@ class TestBlkid(CiTestCase): """ ) - maxDiff = None - def _get_expected(self): return { "/dev/loop0": {"DEVNAME": "/dev/loop0", "TYPE": "squashfs"}, @@ -898,7 +884,7 @@ def _get_expected(self): @mock.patch("cloudinit.subp.subp") def test_functional_blkid(self, m_subp): m_subp.return_value = SubpResult(self.blkid_out.format(**self.ids), "") - self.assertEqual(self._get_expected(), util.blkid()) + assert self._get_expected() == util.blkid() m_subp.assert_called_with( ["blkid", "-o", "full"], capture=True, decode="replace" ) @@ -907,7 +893,7 @@ def test_functional_blkid(self, m_subp): def test_blkid_no_cache_uses_no_cache(self, m_subp): """blkid should turn off cache if disable_cache is true.""" m_subp.return_value = SubpResult(self.blkid_out.format(**self.ids), "") - self.assertEqual(self._get_expected(), util.blkid(disable_cache=True)) + assert self._get_expected() == util.blkid(disable_cache=True) m_subp.assert_called_with( ["blkid", "-o", "full", "-c", "/dev/null"], capture=True, @@ -917,7 +903,7 @@ def test_blkid_no_cache_uses_no_cache(self, m_subp): @mock.patch("cloudinit.util.subp.which") @mock.patch("cloudinit.util.subp.subp") -class TestUdevadmSettle(CiTestCase): +class TestUdevadmSettle: def test_with_no_params(self, m_subp, m_which): """called with no parameters.""" m_which.side_effect = lambda m: m in ("udevadm",) @@ -931,20 +917,20 @@ def test_udevadm_not_present(self, m_subp, m_which): m_which.assert_called_once_with("udevadm") m_subp.assert_not_called() - def test_with_exists_and_not_exists(self, m_subp, m_which): + def test_with_exists_and_not_exists(self, m_subp, m_which, tmp_path): """with exists=file where file does not exist should invoke subp.""" m_which.side_effect = lambda m: m in ("udevadm",) - mydev = self.tmp_path("mydev") + mydev = str(tmp_path / "mydev") util.udevadm_settle(exists=mydev) m_subp.assert_called_once_with( ["udevadm", "settle", "--exit-if-exists=%s" % mydev] ) - def test_with_exists_and_file_exists(self, m_subp, m_which): + def test_with_exists_and_file_exists(self, m_subp, m_which, tmp_path): """with exists=file where file does exist should only invoke subp once for 'which' call.""" m_which.side_effect = lambda m: m in ("udevadm",) - mydev = self.tmp_path("mydev") + mydev = str(tmp_path / "mydev") util.write_file(mydev, "foo\n") util.udevadm_settle(exists=mydev) m_which.assert_called_once_with("udevadm") @@ -968,10 +954,10 @@ def test_with_timeout_string(self, m_subp, m_which): ["udevadm", "settle", "--timeout=%s" % timeout] ) - def test_with_exists_and_timeout(self, m_subp, m_which): + def test_with_exists_and_timeout(self, m_subp, m_which, tmp_path): """test call with both exists and timeout.""" m_which.side_effect = lambda m: m in ("udevadm",) - mydev = self.tmp_path("mydev") + mydev = str(tmp_path / "mydev") timeout = "3" util.udevadm_settle(exists=mydev, timeout=timeout) m_subp.assert_called_once_with( @@ -986,12 +972,14 @@ def test_with_exists_and_timeout(self, m_subp, m_which): def test_subp_exception_raises_to_caller(self, m_subp, m_which): m_which.side_effect = lambda m: m in ("udevadm",) m_subp.side_effect = subp.ProcessExecutionError("BOOM") - self.assertRaises(subp.ProcessExecutionError, util.udevadm_settle) + with pytest.raises(subp.ProcessExecutionError): + util.udevadm_settle() @mock.patch("os.path.exists") -class TestGetLinuxDistro(CiTestCase): - def setUp(self): +class TestGetLinuxDistro: + @pytest.fixture(autouse=True) + def fixtures(self): util.get_linux_distro.cache_clear() @classmethod @@ -1013,7 +1001,7 @@ def test_get_linux_distro_quoted_name(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_SLES m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("sles", "12.3", platform.machine()), dist) + assert ("sles", "12.3", platform.machine()) == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_distro_bare_name(self, m_os_release, m_path_exists): @@ -1022,7 +1010,7 @@ def test_get_linux_distro_bare_name(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_UBUNTU m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("ubuntu", "16.04", "xenial"), dist) + assert ("ubuntu", "16.04", "xenial") == dist @mock.patch("platform.system") @mock.patch("platform.release") @@ -1041,7 +1029,7 @@ def test_get_linux_freebsd( m_parse_redhat_release.return_value = {} util.is_BSD.cache_clear() dist = util.get_linux_distro() - self.assertEqual(("freebsd", "12.0-RELEASE-p10", ""), dist) + assert ("freebsd", "12.0-RELEASE-p10", "") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_centos6(self, m_os_release, m_path_exists): @@ -1049,7 +1037,7 @@ def test_get_linux_centos6(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_CENTOS_6 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("centos", "6.10", "Final"), dist) + assert ("centos", "6.10", "Final") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_centos7_redhat_release(self, m_os_release, m_exists): @@ -1057,7 +1045,7 @@ def test_get_linux_centos7_redhat_release(self, m_os_release, m_exists): m_os_release.return_value = REDHAT_RELEASE_CENTOS_7 m_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("centos", "7.5.1804", "Core"), dist) + assert ("centos", "7.5.1804", "Core") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_redhat7_osrelease(self, m_os_release, m_path_exists): @@ -1065,7 +1053,7 @@ def test_get_linux_redhat7_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_REDHAT_7 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("redhat", "7.5", "Maipo"), dist) + assert ("redhat", "7.5", "Maipo") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_redhat7_rhrelease(self, m_os_release, m_path_exists): @@ -1073,7 +1061,7 @@ def test_get_linux_redhat7_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_REDHAT_7 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("redhat", "7.5", "Maipo"), dist) + assert ("redhat", "7.5", "Maipo") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_redhat6_rhrelease(self, m_os_release, m_path_exists): @@ -1081,7 +1069,7 @@ def test_get_linux_redhat6_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_REDHAT_6 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("redhat", "6.10", "Santiago"), dist) + assert ("redhat", "6.10", "Santiago") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_copr_centos(self, m_os_release, m_path_exists): @@ -1089,7 +1077,7 @@ def test_get_linux_copr_centos(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_CENTOS m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("centos", "7", "Core"), dist) + assert ("centos", "7", "Core") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_almalinux8_rhrelease(self, m_os_release, m_path_exists): @@ -1097,7 +1085,7 @@ def test_get_linux_almalinux8_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_ALMALINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("almalinux", "8.3", "Purple Manul"), dist) + assert ("almalinux", "8.3", "Purple Manul") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_almalinux8_osrelease(self, m_os_release, m_path_exists): @@ -1105,7 +1093,7 @@ def test_get_linux_almalinux8_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_ALMALINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("almalinux", "8.3", "Purple Manul"), dist) + assert ("almalinux", "8.3", "Purple Manul") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_eurolinux7_rhrelease(self, m_os_release, m_path_exists): @@ -1113,7 +1101,7 @@ def test_get_linux_eurolinux7_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_7 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("eurolinux", "7.9", "Minsk"), dist) + assert ("eurolinux", "7.9", "Minsk") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_eurolinux7_osrelease(self, m_os_release, m_path_exists): @@ -1121,7 +1109,7 @@ def test_get_linux_eurolinux7_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_EUROLINUX_7 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("eurolinux", "7.9", "Minsk"), dist) + assert ("eurolinux", "7.9", "Minsk") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_eurolinux8_rhrelease(self, m_os_release, m_path_exists): @@ -1129,7 +1117,7 @@ def test_get_linux_eurolinux8_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("eurolinux", "8.4", "Vaduz"), dist) + assert ("eurolinux", "8.4", "Vaduz") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_eurolinux8_osrelease(self, m_os_release, m_path_exists): @@ -1137,7 +1125,7 @@ def test_get_linux_eurolinux8_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_EUROLINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("eurolinux", "8.4", "Vaduz"), dist) + assert ("eurolinux", "8.4", "Vaduz") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_miraclelinux8_rhrelease( @@ -1147,7 +1135,7 @@ def test_get_linux_miraclelinux8_rhrelease( m_os_release.return_value = REDHAT_RELEASE_MIRACLELINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("miracle", "8.4", "Peony"), dist) + assert ("miracle", "8.4", "Peony") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_miraclelinux8_osrelease( @@ -1157,7 +1145,7 @@ def test_get_linux_miraclelinux8_osrelease( m_os_release.return_value = OS_RELEASE_MIRACLELINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("miraclelinux", "8", "Peony"), dist) + assert ("miraclelinux", "8", "Peony") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): @@ -1165,7 +1153,7 @@ def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_ROCKY_8 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("rocky", "8.3", "Green Obsidian"), dist) + assert ("rocky", "8.3", "Green Obsidian") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_rocky8_osrelease(self, m_os_release, m_path_exists): @@ -1173,7 +1161,7 @@ def test_get_linux_rocky8_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_ROCKY_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("rocky", "8.3", "Green Obsidian"), dist) + assert ("rocky", "8.3", "Green Obsidian") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_virtuozzo8_rhrelease(self, m_os_release, m_path_exists): @@ -1181,7 +1169,7 @@ def test_get_linux_virtuozzo8_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_VIRTUOZZO_8 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("virtuozzo", "8", "Virtuozzo Linux"), dist) + assert ("virtuozzo", "8", "Virtuozzo Linux") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_virtuozzo8_osrelease(self, m_os_release, m_path_exists): @@ -1189,7 +1177,7 @@ def test_get_linux_virtuozzo8_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_VIRTUOZZO_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("virtuozzo", "8", "Virtuozzo Linux"), dist) + assert ("virtuozzo", "8", "Virtuozzo Linux") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_cloud8_rhrelease(self, m_os_release, m_path_exists): @@ -1197,7 +1185,7 @@ def test_get_linux_cloud8_rhrelease(self, m_os_release, m_path_exists): m_os_release.return_value = REDHAT_RELEASE_CLOUDLINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists dist = util.get_linux_distro() - self.assertEqual(("cloudlinux", "8.4", "Valery Rozhdestvensky"), dist) + assert ("cloudlinux", "8.4", "Valery Rozhdestvensky") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_cloud8_osrelease(self, m_os_release, m_path_exists): @@ -1205,7 +1193,7 @@ def test_get_linux_cloud8_osrelease(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_CLOUDLINUX_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("cloudlinux", "8.4", "Valery Rozhdestvensky"), dist) + assert ("cloudlinux", "8.4", "Valery Rozhdestvensky") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_debian(self, m_os_release, m_path_exists): @@ -1213,7 +1201,7 @@ def test_get_linux_debian(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_DEBIAN m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("debian", "9", "stretch"), dist) + assert ("debian", "9", "stretch") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_openeuler(self, m_os_release, m_path_exists): @@ -1221,7 +1209,7 @@ def test_get_linux_openeuler(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_OPENEULER_20 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("openEuler", "20.03", "LTS-SP2"), dist) + assert ("openEuler", "20.03", "LTS-SP2") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_opencloudos(self, m_os_release, m_path_exists): @@ -1229,7 +1217,7 @@ def test_get_linux_opencloudos(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_OPENCLOUDOS_8 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("OpenCloudOS", "8.6", ""), dist) + assert ("OpenCloudOS", "8.6", "") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_tencentos(self, m_os_release, m_path_exists): @@ -1237,7 +1225,7 @@ def test_get_linux_tencentos(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_TENCENTOS_3 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("TencentOS", "3.1", ""), dist) + assert ("TencentOS", "3.1", "") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_opensuse(self, m_os_release, m_path_exists): @@ -1247,7 +1235,7 @@ def test_get_linux_opensuse(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_OPENSUSE m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("opensuse", "42.3", platform.machine()), dist) + assert ("opensuse", "42.3", platform.machine()) == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_opensuse_l15(self, m_os_release, m_path_exists): @@ -1257,7 +1245,7 @@ def test_get_linux_opensuse_l15(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_OPENSUSE_L15 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("opensuse-leap", "15.0", platform.machine()), dist) + assert ("opensuse-leap", "15.0", platform.machine()) == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_opensuse_tw(self, m_os_release, m_path_exists): @@ -1267,9 +1255,7 @@ def test_get_linux_opensuse_tw(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_OPENSUSE_TW m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual( - ("opensuse-tumbleweed", "20180920", platform.machine()), dist - ) + assert ("opensuse-tumbleweed", "20180920", platform.machine()) == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_photon_os_release(self, m_os_release, m_path_exists): @@ -1277,7 +1263,7 @@ def test_get_linux_photon_os_release(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_PHOTON m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("photon", "4.0", "VMware Photon OS/Linux"), dist) + assert ("photon", "4.0", "VMware Photon OS/Linux") == dist @mock.patch("cloudinit.util.load_text_file") def test_get_linux_mariner_os_release(self, m_os_release, m_path_exists): @@ -1285,7 +1271,7 @@ def test_get_linux_mariner_os_release(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_MARINER m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("mariner", "2.0", ""), dist) + assert ("mariner", "2.0", "") == dist @mock.patch("cloudinit.util.load_text_file") def test_get_linux_azurelinux_os_release( @@ -1295,7 +1281,7 @@ def test_get_linux_azurelinux_os_release( m_os_release.return_value = OS_RELEASE_AZURELINUX m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("azurelinux", "3.0", ""), dist) + assert ("azurelinux", "3.0", "") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_openmandriva(self, m_os_release, m_path_exists): @@ -1303,7 +1289,7 @@ def test_get_linux_openmandriva(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_OPENMANDRIVA m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("openmandriva", "4.90", "nickel"), dist) + assert ("openmandriva", "4.90", "nickel") == dist @mock.patch(M_PATH + "load_text_file") def test_get_linux_cos(self, m_os_release, m_path_exists): @@ -1311,7 +1297,7 @@ def test_get_linux_cos(self, m_os_release, m_path_exists): m_os_release.return_value = OS_RELEASE_COS m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists dist = util.get_linux_distro() - self.assertEqual(("cos", "93", ""), dist) + assert ("cos", "93", "") == dist @mock.patch("platform.system") @mock.patch("platform.dist", create=True) @@ -1323,7 +1309,7 @@ def test_get_linux_distro_no_data( m_platform_system.return_value = "Linux" m_path_exists.return_value = 0 dist = util.get_linux_distro() - self.assertEqual(("", "", ""), dist) + assert ("", "", "") == dist @mock.patch("platform.system") @mock.patch("platform.dist", create=True) @@ -1336,7 +1322,7 @@ def test_get_linux_distro_no_impl( m_platform_system.return_value = "Linux" m_path_exists.return_value = 0 dist = util.get_linux_distro() - self.assertEqual(("", "", ""), dist) + assert ("", "", "") == dist @mock.patch("platform.system") @mock.patch("platform.dist", create=True) @@ -1348,7 +1334,7 @@ def test_get_linux_distro_plat_data( m_platform_system.return_value = "Linux" m_path_exists.return_value = 0 dist = util.get_linux_distro() - self.assertEqual(("foo", "1.1", "aarch64"), dist) + assert ("foo", "1.1", "aarch64") == dist class TestGetVariant: @@ -1396,40 +1382,36 @@ def test_get_variant(self, info, expected_variant): assert util._get_variant(info) == expected_variant -class TestJsonDumps(CiTestCase): +class TestJsonDumps: def test_is_str(self): """json_dumps should return a string.""" - self.assertTrue( - isinstance(atomic_helper.json_dumps({"abc": "123"}), str) - ) + assert isinstance(atomic_helper.json_dumps({"abc": "123"}), str) def test_utf8(self): smiley = "\\ud83d\\ude03" - self.assertEqual( - {"smiley": smiley}, - json.loads(atomic_helper.json_dumps({"smiley": smiley})), + assert {"smiley": smiley} == json.loads( + atomic_helper.json_dumps({"smiley": smiley}) ) def test_non_utf8(self): blob = b"\xba\x03Qx-#y\xea" - self.assertEqual( - {"blob": "ci-b64:" + base64.b64encode(blob).decode("utf-8")}, - json.loads(atomic_helper.json_dumps({"blob": blob})), - ) + assert { + "blob": "ci-b64:" + base64.b64encode(blob).decode("utf-8") + } == json.loads(atomic_helper.json_dumps({"blob": blob})) @mock.patch("os.path.exists") -class TestIsLXD(CiTestCase): +class TestIsLXD: def test_is_lxd_true_on_sock_device(self, m_exists): """When lxd's /dev/lxd/sock exists, is_lxd returns true.""" m_exists.return_value = True - self.assertTrue(util.is_lxd()) + assert util.is_lxd() is True m_exists.assert_called_once_with("/dev/lxd/sock") def test_is_lxd_false_when_sock_device_absent(self, m_exists): """When lxd's /dev/lxd/sock is absent, is_lxd returns false.""" m_exists.return_value = False - self.assertFalse(util.is_lxd()) + assert not util.is_lxd() m_exists.assert_called_once_with("/dev/lxd/sock") @@ -1438,7 +1420,7 @@ class TestReadCcFromCmdline: "cmdline,expected_cfg", [ # Return None if cmdline has no cc:end_cc content. - pytest.param(CiTestCase.random_string(), None, id="random_string"), + pytest.param(random_string(), None, id="random_string"), # Return None if YAML content is empty string. ("foo cc: end_cc bar", None), # Return expected dictionary without trailing end_cc marker. @@ -1813,31 +1795,31 @@ def test_not_found_no_default(self): """None is returned if key is not found and no default given.""" config = {} result = util.get_cfg_option_list(config, "key") - self.assertIsNone(result) + assert result is None def test_not_found_with_default(self): """Default is returned if key is not found.""" config = {} result = util.get_cfg_option_list(config, "key", default=["DEFAULT"]) - self.assertEqual(["DEFAULT"], result) + assert ["DEFAULT"] == result def test_found_with_default(self): """Default is not returned if key is found.""" config = {"key": ["value1"]} result = util.get_cfg_option_list(config, "key", default=["DEFAULT"]) - self.assertEqual(["value1"], result) + assert ["value1"] == result def test_found_convert_to_list(self): """Single string is converted to one element list.""" config = {"key": "value1"} result = util.get_cfg_option_list(config, "key") - self.assertEqual(["value1"], result) + assert ["value1"] == result def test_value_is_none(self): """If value is None empty list is returned.""" config = {"key": None} result = util.get_cfg_option_list(config, "key") - self.assertEqual([], result) + assert [] == result class TestWriteFile(helpers.TestCase): @@ -1853,13 +1835,13 @@ def test_basic_usage(self): util.write_file(path, contents) - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isfile(path)) + assert os.path.exists(path) + assert os.path.isfile(path) with open(path) as f: create_contents = f.read() - self.assertEqual(contents, create_contents) + assert contents == create_contents file_stat = os.stat(path) - self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode)) + assert 0o644 == stat.S_IMODE(file_stat.st_mode) def test_dir_is_created_if_required(self): """Verifiy that directories are created is required.""" @@ -1869,8 +1851,8 @@ def test_dir_is_created_if_required(self): util.write_file(path, contents) - self.assertTrue(os.path.isdir(dirname)) - self.assertTrue(os.path.isfile(path)) + assert os.path.isdir(dirname) + assert os.path.isfile(path) def test_dir_ownership(self): """Verifiy that directories is created with appropriate ownership.""" @@ -1897,10 +1879,10 @@ def test_dir_is_not_created_if_ensure_dir_false(self): path = os.path.join(dirname, "NewFile.txt") contents = "Hey there" - with self.assertRaises(FileNotFoundError): + with pytest.raises(FileNotFoundError): util.write_file(path, contents, ensure_dir_exists=False) - self.assertFalse(os.path.isdir(dirname)) + assert not os.path.isdir(dirname) def test_explicit_mode(self): """Verify explicit file mode works properly.""" @@ -1909,10 +1891,10 @@ def test_explicit_mode(self): util.write_file(path, contents, mode=0o666) - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isfile(path)) + assert os.path.exists(path) + assert os.path.isfile(path) file_stat = os.stat(path) - self.assertEqual(0o666, stat.S_IMODE(file_stat.st_mode)) + assert 0o666 == stat.S_IMODE(file_stat.st_mode) def test_preserve_mode_no_existing(self): """Verify that file is created with mode 0o644 if preserve_mode @@ -1922,10 +1904,10 @@ def test_preserve_mode_no_existing(self): util.write_file(path, contents, preserve_mode=True) - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isfile(path)) + assert os.path.exists(path) + assert os.path.isfile(path) file_stat = os.stat(path) - self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode)) + assert 0o644 == stat.S_IMODE(file_stat.st_mode) def test_preserve_mode_with_existing(self): """Verify that file is created using mode of existing file @@ -1938,10 +1920,10 @@ def test_preserve_mode_with_existing(self): util.write_file(path, contents, preserve_mode=True) - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isfile(path)) + assert os.path.exists(path) + assert os.path.isfile(path) file_stat = os.stat(path) - self.assertEqual(0o666, stat.S_IMODE(file_stat.st_mode)) + assert 0o666 == stat.S_IMODE(file_stat.st_mode) def test_custom_omode(self): """Verify custom omode works properly.""" @@ -1953,11 +1935,11 @@ def test_custom_omode(self): f.write(b"LINE1\n") util.write_file(path, contents, omode="a") - self.assertTrue(os.path.exists(path)) - self.assertTrue(os.path.isfile(path)) + assert os.path.exists(path) + assert os.path.isfile(path) with open(path) as f: create_contents = f.read() - self.assertEqual("LINE1\nHey there", create_contents) + assert "LINE1\nHey there" == create_contents def test_restorecon_if_possible_is_called(self): """Make sure the selinux guard is called correctly.""" @@ -1971,10 +1953,10 @@ def test_restorecon_if_possible_is_called(self): importer, "import_module", return_value=fake_se ) as mockobj: with util.SeLinuxGuard(my_file) as is_on: - self.assertTrue(is_on) + assert is_on - self.assertEqual(1, len(fake_se.restored)) - self.assertEqual(my_file, fake_se.restored[0]) + assert 1 == len(fake_se.restored) + assert my_file == fake_se.restored[0] mockobj.assert_called_once_with("selinux") @@ -1986,13 +1968,13 @@ def setUp(self): self.addCleanup(shutil.rmtree, self.tmp) def assertDirEmpty(self, dirname): - self.assertEqual([], os.listdir(dirname)) + assert [] == os.listdir(dirname) def test_does_not_delete_dir(self): """Ensure directory itself is not deleted.""" util.delete_dir_contents(self.tmp) - self.assertTrue(os.path.isdir(self.tmp)) + assert os.path.isdir(self.tmp) self.assertDirEmpty(self.tmp) def test_deletes_files(self): @@ -2049,7 +2031,7 @@ class TestKeyValStrings(helpers.TestCase): def test_keyval_str_to_dict(self): expected = {"1": "one", "2": "one+one", "ro": True} cmdline = "1=one ro 2=one+one" - self.assertEqual(expected, util.keyval_str_to_dict(cmdline)) + assert expected == util.keyval_str_to_dict(cmdline) class TestGetCmdline(helpers.TestCase): @@ -2058,7 +2040,7 @@ def test_cmdline_reads_debug_env(self): "os.environ", values={"DEBUG_PROC_CMDLINE": "abcd 123"} ): ret = util.get_cmdline() - self.assertEqual("abcd 123", ret) + assert "abcd 123" == ret class TestFipsEnabled: @@ -2086,57 +2068,49 @@ def fake_load_file(path): assert expected is util.fips_enabled() -class TestLoadYaml(helpers.CiTestCase): +class TestLoadYaml: mydefault = "7b03a8ebace993d806255121073fed52" - with_logs = True def test_simple(self): mydata = {"1": "one", "2": "two"} - self.assertEqual(util.load_yaml(yaml.dump(mydata)), mydata) + assert util.load_yaml(yaml.dump(mydata)) == mydata - def test_nonallowed_returns_default(self): + def test_nonallowed_returns_default(self, caplog): """Any unallowed types result in returning default; log the issue.""" # for now, anything not in the allowed list just returns the default. myyaml = yaml.dump({"1": "one"}) - self.assertEqual( - util.load_yaml( - blob=myyaml, default=self.mydefault, allowed=(str,) - ), - self.mydefault, + assert ( + util.load_yaml(blob=myyaml, default=self.mydefault, allowed=(str,)) + == self.mydefault ) regex = re.compile( r"Yaml load allows \(<(class|type) \'str\'>,\) root types, but" r" got dict" ) - self.assertTrue( - regex.search(self.logs.getvalue()), - msg="Missing expected yaml load error", - ) + assert regex.search(caplog.text), "Missing expected yaml load error" - def test_bogus_scan_error_returns_default(self): + def test_bogus_scan_error_returns_default(self, caplog): """On Yaml scan error, load_yaml returns the default and logs issue.""" badyaml = "1\n 2:" - self.assertEqual( - util.load_yaml(blob=badyaml, default=self.mydefault), - self.mydefault, + assert ( + util.load_yaml(blob=badyaml, default=self.mydefault) + == self.mydefault ) - self.assertIn( + assert ( "Failed loading yaml blob. Invalid format at line 2 column 3:" - ' "mapping values are not allowed here', - self.logs.getvalue(), + ' "mapping values are not allowed here' in caplog.text ) - def test_bogus_parse_error_returns_default(self): + def test_bogus_parse_error_returns_default(self, caplog): """On Yaml parse error, load_yaml returns default and logs issue.""" badyaml = "{}}" - self.assertEqual( - util.load_yaml(blob=badyaml, default=self.mydefault), - self.mydefault, + assert ( + util.load_yaml(blob=badyaml, default=self.mydefault) + == self.mydefault ) - self.assertIn( + assert ( "Failed loading yaml blob. Invalid format at line 1 column 3:" - " \"expected '', but found '}'", - self.logs.getvalue(), + " \"expected '', but found '}'" in caplog.text ) def test_unsafe_types(self): @@ -2148,27 +2122,24 @@ def test_unsafe_types(self): 3, ) ) - self.assertEqual( - util.load_yaml(blob=unsafe_yaml, default=self.mydefault), - self.mydefault, + assert ( + util.load_yaml(blob=unsafe_yaml, default=self.mydefault) + == self.mydefault ) def test_python_unicode(self): # complex type of python/unicode is explicitly allowed myobj = {"1": "FOOBAR"} safe_yaml = yaml.dump(myobj) - self.assertEqual( - util.load_yaml(blob=safe_yaml, default=self.mydefault), myobj - ) + assert util.load_yaml(blob=safe_yaml, default=self.mydefault) == myobj def test_none_returns_default(self): """If yaml.load returns None, then default should be returned.""" blobs = ("", " ", "# foo\n", "#") mdef = self.mydefault - self.assertEqual( - [(b, self.mydefault) for b in blobs], - [(b, util.load_yaml(blob=b, default=mdef)) for b in blobs], - ) + assert [(b, self.mydefault) for b in blobs] == [ + (b, util.load_yaml(blob=b, default=mdef)) for b in blobs + ] class TestMountinfoParsing: @@ -2264,106 +2235,91 @@ def test_parse_mount_with_zfs(self, mount_out): assert ("vmzroot/var/tmp", "zfs", "/var/tmp") == ret -class TestIsX86(helpers.CiTestCase): +class TestIsX86: def test_is_x86_matches_x86_types(self): """is_x86 returns True if CPU architecture matches.""" matched_arches = ["x86_64", "i386", "i586", "i686"] for arch in matched_arches: - self.assertTrue( - util.is_x86(arch), 'Expected is_x86 for arch "%s"' % arch - ) + assert util.is_x86(arch), 'Expected is_x86 for arch "%s"' % arch def test_is_x86_unmatched_types(self): """is_x86 returns Fale on non-intel x86 architectures.""" unmatched_arches = ["ia64", "9000/800", "arm64v71"] for arch in unmatched_arches: - self.assertFalse( - util.is_x86(arch), 'Expected not is_x86 for arch "%s"' % arch + assert not util.is_x86(arch), ( + 'Expected not is_x86 for arch "%s"' % arch ) @mock.patch(M_PATH + "os.uname") def test_is_x86_calls_uname_for_architecture(self, m_uname): """is_x86 returns True if platform from uname matches.""" m_uname.return_value = [0, 1, 2, 3, "x86_64"] - self.assertTrue(util.is_x86()) + assert util.is_x86() -class TestGetConfigLogfiles(helpers.CiTestCase): +class TestGetConfigLogfiles: def test_empty_cfg_returns_empty_list(self): """An empty config passed to get_config_logfiles returns empty list.""" - self.assertEqual([], util.get_config_logfiles(None)) - self.assertEqual([], util.get_config_logfiles({})) + assert [] == util.get_config_logfiles(None) + assert [] == util.get_config_logfiles({}) def test_default_log_file_present(self): """When default_log_file is set get_config_logfiles finds it.""" - self.assertEqual( - ["/my.log"], util.get_config_logfiles({"def_log_file": "/my.log"}) + assert ["/my.log"] == util.get_config_logfiles( + {"def_log_file": "/my.log"} ) def test_output_logs_parsed_when_teeing_files(self): """When output configuration is parsed when teeing files.""" - self.assertEqual( - ["/himom.log", "/my.log"], - sorted( - util.get_config_logfiles( - { - "def_log_file": "/my.log", - "output": {"all": "|tee -a /himom.log"}, - } - ) - ), + assert ["/himom.log", "/my.log"] == sorted( + util.get_config_logfiles( + { + "def_log_file": "/my.log", + "output": {"all": "|tee -a /himom.log"}, + } + ) ) def test_output_logs_parsed_when_redirecting(self): """When output configuration is parsed when redirecting to a file.""" - self.assertEqual( - ["/my.log", "/test.log"], - sorted( - util.get_config_logfiles( - { - "def_log_file": "/my.log", - "output": {"all": ">/test.log"}, - } - ) - ), + assert ["/my.log", "/test.log"] == sorted( + util.get_config_logfiles( + { + "def_log_file": "/my.log", + "output": {"all": ">/test.log"}, + } + ) ) def test_output_logs_parsed_when_appending(self): """When output configuration is parsed when appending to a file.""" - self.assertEqual( - ["/my.log", "/test.log"], - sorted( - util.get_config_logfiles( - { - "def_log_file": "/my.log", - "output": {"all": ">> /test.log"}, - } - ) - ), + assert ["/my.log", "/test.log"] == sorted( + util.get_config_logfiles( + { + "def_log_file": "/my.log", + "output": {"all": ">> /test.log"}, + } + ) ) - def test_output_logs_parsed_when_teeing_files_and_rotated(self): + def test_output_logs_parsed_when_teeing_files_and_rotated(self, tmp_path): """When output configuration is parsed when teeing files and rotated log files are present.""" - tmpd = self.tmp_dir() - log1 = self.tmp_path("my.log", tmpd) - log1_rotated = self.tmp_path("my.log.1.gz", tmpd) - log2 = self.tmp_path("himom.log", tmpd) - log2_rotated = self.tmp_path("himom.log.1.gz", tmpd) + log1 = str(tmp_path / "my.log") + log1_rotated = str(tmp_path / "my.log.1.gz") + log2 = str(tmp_path / "himom.log") + log2_rotated = str(tmp_path / "himom.log.1.gz") util.write_file(log1_rotated, "hello") util.write_file(log2_rotated, "hello") - self.assertEqual( - [log2, log2_rotated, log1, log1_rotated], - sorted( - util.get_config_logfiles( - { - "def_log_file": str(log1), - "output": {"all": f"|tee -a {log2}"}, - } - ) - ), + assert [log2, log2_rotated, log1, log1_rotated] == sorted( + util.get_config_logfiles( + { + "def_log_file": str(log1), + "output": {"all": f"|tee -a {log2}"}, + } + ) ) @@ -2461,7 +2417,7 @@ def test_given_log_level_used(self): class TestMessageFromString(helpers.TestCase): def test_unicode_not_messed_up(self): roundtripped = util.message_from_string("\n").as_string() - self.assertNotIn("\x00", roundtripped) + assert "\x00" not in roundtripped class TestReadOptionalSeed: @@ -2663,10 +2619,10 @@ def test_unicode_not_messed_up(self): sdir = self.tmp + os.path.sep found_md, found_ud, found_vd, found_network = util.read_seeded(sdir) - self.assertEqual(found_md, {"key1": "val1"}) - self.assertEqual(found_ud, ud) - self.assertEqual(found_vd, vd) - self.assertEqual(found_network, {"test": "true"}) + assert found_md == {"key1": "val1"} + assert found_ud == ud + assert found_vd == vd + assert found_network == {"test": "true"} class TestEncode(helpers.TestCase): @@ -2675,7 +2631,7 @@ class TestEncode(helpers.TestCase): def test_decode_binary_plain_text_with_hex(self): blob = "BOOTABLE_FLAG=\x80init=/bin/systemd" text = util.decode_binary(blob) - self.assertEqual(text, blob) + assert text == blob class TestProcessExecutionError(helpers.TestCase): @@ -2694,37 +2650,30 @@ def test_pexec_error_indent_text(self): error = subp.ProcessExecutionError() msg = "abc\ndef" formatted = "abc\n{0}def".format(" " * 4) - self.assertEqual(error._indent_text(msg, indent_level=4), formatted) - self.assertEqual( - error._indent_text(msg.encode(), indent_level=4), - formatted.encode(), - ) - self.assertIsInstance( - error._indent_text(msg.encode()), type(msg.encode()) + assert error._indent_text(msg, indent_level=4) == formatted + assert ( + error._indent_text(msg.encode(), indent_level=4) + == formatted.encode() ) + assert isinstance(error._indent_text(msg.encode()), type(msg.encode())) def test_pexec_error_type(self): - self.assertIsInstance(subp.ProcessExecutionError(), IOError) + assert isinstance(subp.ProcessExecutionError(), IOError) def test_pexec_error_empty_msgs(self): error = subp.ProcessExecutionError() - self.assertTrue( - all( - attr == self.empty_attr - for attr in (error.stderr, error.stdout, error.reason) - ) + assert all( + attr == self.empty_attr + for attr in (error.stderr, error.stdout, error.reason) ) - self.assertEqual(error.description, self.empty_description) - self.assertEqual( - str(error), - self.template.format( - description=self.empty_description, - exit_code=self.empty_attr, - reason=self.empty_attr, - stdout=self.empty_attr, - stderr=self.empty_attr, - cmd=self.empty_attr, - ), + assert error.description == self.empty_description + assert str(error) == self.template.format( + description=self.empty_description, + exit_code=self.empty_attr, + reason=self.empty_attr, + stdout=self.empty_attr, + stderr=self.empty_attr, + cmd=self.empty_attr, ) def test_pexec_error_single_line_msgs(self): @@ -2735,16 +2684,13 @@ def test_pexec_error_single_line_msgs(self): error = subp.ProcessExecutionError( stdout=stdout_msg, stderr=stderr_msg, exit_code=3, cmd=cmd ) - self.assertEqual( - str(error), - self.template.format( - description=self.empty_description, - stdout=stdout_msg, - stderr=stderr_msg, - exit_code=str(exit_code), - reason=self.empty_attr, - cmd=cmd, - ), + assert str(error) == self.template.format( + description=self.empty_description, + stdout=stdout_msg, + stderr=stderr_msg, + exit_code=str(exit_code), + reason=self.empty_attr, + cmd=cmd, ) def test_pexec_error_multi_line_msgs(self): @@ -2754,24 +2700,21 @@ def test_pexec_error_multi_line_msgs(self): error = subp.ProcessExecutionError( stdout=stdout_msg, stderr=stderr_msg ) - self.assertEqual( - str(error), - "\n".join( - ( - "{description}", - "Command: {empty_attr}", - "Exit code: {empty_attr}", - "Reason: {empty_attr}", - "Stdout: multi", - " line", - " output message", - "Stderr: multi", - " line", - " error message", - ) - ).format( - description=self.empty_description, empty_attr=self.empty_attr - ), + assert str(error) == "\n".join( + ( + "{description}", + "Command: {empty_attr}", + "Exit code: {empty_attr}", + "Reason: {empty_attr}", + "Stdout: multi", + " line", + " output message", + "Stderr: multi", + " line", + " error message", + ) + ).format( + description=self.empty_description, empty_attr=self.empty_attr ) @@ -2835,20 +2778,21 @@ def test_system_image_config_dir_is_snappy(self, mocker): class TestLoadShellContent(helpers.TestCase): def test_comments_handled_correctly(self): """Shell comments should be allowed in the content.""" - self.assertEqual( - {"key1": "val1", "key2": "val2", "key3": "val3 #tricky"}, - util.load_shell_content( - "\n".join( - [ - "#top of file comment", - "key1=val1 #this is a comment", - "# second comment", - 'key2="val2" # inlin comment#badkey=wark', - 'key3="val3 #tricky"', - "", - ] - ) - ), + assert { + "key1": "val1", + "key2": "val2", + "key3": "val3 #tricky", + } == util.load_shell_content( + "\n".join( + [ + "#top of file comment", + "key1=val1 #this is a comment", + "# second comment", + 'key2="val2" # inlin comment#badkey=wark', + 'key3="val3 #tricky"', + "", + ] + ) ) @@ -2876,34 +2820,29 @@ def test_non_utf8_in_environment(self, m_load_file): ) m_load_file.return_value = content - self.assertEqual( - { - "BOOTABLE_FLAG": self._val_decoded(self.bootflag), - "HOME": "/", - "PATH": "/bin:/sbin", - "MIXED": self._val_decoded(self.mixed), - }, - util.get_proc_env(1), - ) - self.assertEqual(1, m_load_file.call_count) + assert { + "BOOTABLE_FLAG": self._val_decoded(self.bootflag), + "HOME": "/", + "PATH": "/bin:/sbin", + "MIXED": self._val_decoded(self.mixed), + } == util.get_proc_env(1) + assert 1 == m_load_file.call_count @mock.patch(M_PATH + "load_binary_file") def test_all_utf8_encoded(self, m_load_file): """common path where only utf-8 decodable content.""" content = self.null.join((self.simple1, self.simple2)) m_load_file.return_value = content - self.assertEqual( - {"HOME": "/", "PATH": "/bin:/sbin"}, util.get_proc_env(1) - ) - self.assertEqual(1, m_load_file.call_count) + assert {"HOME": "/", "PATH": "/bin:/sbin"} == util.get_proc_env(1) + assert 1 == m_load_file.call_count @mock.patch(M_PATH + "load_binary_file") def test_non_existing_file_returns_empty_dict(self, m_load_file): """as implemented, a non-existing pid returns empty dict. This is how it was originally implemented.""" m_load_file.side_effect = OSError("File does not exist.") - self.assertEqual({}, util.get_proc_env(1)) - self.assertEqual(1, m_load_file.call_count) + assert {} == util.get_proc_env(1) + assert 1 == m_load_file.call_count class TestGetProcPpid(helpers.TestCase): @@ -2914,19 +2853,19 @@ def test_get_proc_ppid_linux(self): """get_proc_ppid returns correct parent pid value.""" my_pid = os.getpid() my_ppid = os.getppid() - self.assertEqual(my_ppid, Distro.get_proc_ppid(my_pid)) + assert my_ppid == Distro.get_proc_ppid(my_pid) @skipIf(not util.is_Linux(), "/proc/$pid/stat is not useful on not-Linux") def test_get_proc_pgrp_linux(self): """get_proc_ppid returns correct parent pid value.""" - self.assertEqual(os.getpgid(0), Distro.get_proc_pgid(os.getpid())) + assert os.getpgid(0) == Distro.get_proc_pgid(os.getpid()) @pytest.mark.allow_subp_for("ps") def test_get_proc_ppid_ps(self): """get_proc_ppid returns correct parent pid value.""" my_pid = os.getpid() my_ppid = os.getppid() - self.assertEqual(my_ppid, Distro.get_proc_ppid(my_pid)) + assert my_ppid == Distro.get_proc_ppid(my_pid) def test_get_proc_ppid_mocked(self): for ppid, proc_data in ( From 0af8dcbfea74fa17a65bf19908c6506c78714fb9 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Fri, 15 Aug 2025 15:20:26 +0200 Subject: [PATCH 079/150] test: drop citestcase net_freebsd (#6374) --- tests/unittests/test_net_freebsd.py | 44 +++++++++++------------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/tests/unittests/test_net_freebsd.py b/tests/unittests/test_net_freebsd.py index 5b21e0e767c..67fe27c5ea7 100644 --- a/tests/unittests/test_net_freebsd.py +++ b/tests/unittests/test_net_freebsd.py @@ -1,10 +1,12 @@ import os +from unittest import mock +import pytest import yaml import cloudinit.net import cloudinit.net.network_state -from tests.unittests.helpers import CiTestCase, dir2dict, mock, readResource +from tests.unittests.helpers import dir2dict, readResource SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output") V1 = """ @@ -29,7 +31,7 @@ """ -class TestInterfacesByMac(CiTestCase): +class TestInterfacesByMac: @mock.patch("cloudinit.subp.subp") @mock.patch("cloudinit.util.is_FreeBSD") def test_get_interfaces_by_mac(self, mock_is_FreeBSD, mock_subp): @@ -44,30 +46,18 @@ def test_get_interfaces_by_mac(self, mock_is_FreeBSD, mock_subp): } -class TestFreeBSDRoundTrip(CiTestCase): - def _render_and_read( - self, network_config=None, state=None, netplan_path=None, target=None - ): - if target is None: - target = self.tmp_dir() - os.mkdir("%s/etc" % target) - with open("%s/etc/rc.conf" % target, "a") as fd: - fd.write("# dummy rc.conf\n") - with open("%s/etc/resolv.conf" % target, "a") as fd: - fd.write("# dummy resolv.conf\n") - - if network_config: - ns = cloudinit.net.network_state.parse_net_config_data( - network_config - ) - elif state: - ns = state - else: - raise ValueError("Expected data or state, got neither") +@pytest.mark.usefixtures("fake_filesystem") +class TestFreeBSDRoundTrip: + def _render_and_read(self, ns): + os.mkdir("/etc") + with open("/etc/rc.conf", "a") as fd: + fd.write("# dummy rc.conf\n") + with open("/etc/resolv.conf", "a") as fd: + fd.write("# dummy resolv.conf\n") renderer = cloudinit.net.freebsd.Renderer() - renderer.render_network_state(ns, target=target) - return dir2dict(target) + renderer.render_network_state(ns) + return dir2dict("/") @mock.patch( "cloudinit.subp.subp", return_value=(SAMPLE_FREEBSD_IFCONFIG_OUT, 0) @@ -79,10 +69,10 @@ def test_render_output_has_yaml(self, m_is_freebsd, m_subp): } network_config = yaml.safe_load(entry["yaml"]) ns = cloudinit.net.network_state.parse_net_config_data(network_config) - files = self._render_and_read(state=ns) + files = self._render_and_read(ns) assert files == { - "/etc/resolv.conf": "# dummy resolv.conf\n", - "/etc/rc.conf": ( + "etc/resolv.conf": "# dummy resolv.conf\n", + "etc/rc.conf": ( "# dummy rc.conf\n" "ifconfig_eno1=" "'inet 172.20.80.129 netmask 255.255.255.128 mtu 1470'\n" From 1c1eebb52343e147d632d734985c3786349d2b0d Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Fri, 15 Aug 2025 10:43:11 -0600 Subject: [PATCH 080/150] fix: add missing test mock (#6395) When files exist in /etc/cloud/clean.d, unit tests fail due to a missing mock. --- tests/unittests/cmd/test_clean.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/unittests/cmd/test_clean.py b/tests/unittests/cmd/test_clean.py index 2f2b6c04bb4..9b7f6824e1c 100644 --- a/tests/unittests/cmd/test_clean.py +++ b/tests/unittests/cmd/test_clean.py @@ -330,11 +330,14 @@ def ds_fetch(): return ds init_class.fetch = ds_fetch - retcode = clean.remove_artifacts( - init_class, - remove_logs=False, - remove_config=["datasource"], - ) + with mock.patch( + "cloudinit.cmd.clean.settings.CLEAN_RUNPARTS_DIR", os.devnull + ): + retcode = clean.remove_artifacts( + init_class, + remove_logs=False, + remove_config=["datasource"], + ) assert ds_conf.exists() is False, f"Unexpected file {ds_conf}" assert 0 == retcode From d094a99eb9fbd03200298c3387f8b1aa6dcbf129 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Fri, 15 Aug 2025 22:23:47 +0200 Subject: [PATCH 081/150] test: drop citestcase from t* tests (#6379) --- tests/unittests/test_temp_utils.py | 28 ++++++------ tests/unittests/test_templating.py | 68 +++++++++++++++--------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/tests/unittests/test_temp_utils.py b/tests/unittests/test_temp_utils.py index 6cbc372823c..670e92aaea2 100644 --- a/tests/unittests/test_temp_utils.py +++ b/tests/unittests/test_temp_utils.py @@ -5,11 +5,13 @@ import os from tempfile import gettempdir +import pytest + from cloudinit.temp_utils import mkdtemp, mkstemp, tempdir -from tests.unittests.helpers import CiTestCase, wrap_and_call +from tests.unittests.helpers import wrap_and_call -class TestTempUtils(CiTestCase): +class TestTempUtils: prefix = gettempdir() def test_mkdtemp_default_non_root(self): @@ -29,8 +31,8 @@ def fake_mkdtemp(*args, **kwargs): }, mkdtemp, ) - self.assertEqual("/fake/return/path", retval) - self.assertEqual([{"dir": self.prefix}], calls) + assert "/fake/return/path" == retval + assert [{"dir": self.prefix}] == calls def test_mkdtemp_default_non_root_needs_exe(self): """mkdtemp creates a dir under /var/tmp/cloud-init when needs_exe.""" @@ -51,8 +53,8 @@ def fake_mkdtemp(*args, **kwargs): mkdtemp, needs_exe=True, ) - self.assertEqual("/fake/return/path", retval) - self.assertEqual([{"dir": "/var/tmp/cloud-init"}], calls) + assert "/fake/return/path" == retval + assert [{"dir": "/var/tmp/cloud-init"}] == calls def test_mkdtemp_default_root(self): """mkdtemp creates a dir under /run/cloud-init for the privileged.""" @@ -71,8 +73,8 @@ def fake_mkdtemp(*args, **kwargs): }, mkdtemp, ) - self.assertEqual("/fake/return/path", retval) - self.assertEqual([{"dir": "/run/cloud-init/tmp"}], calls) + assert "/fake/return/path" == retval + assert [{"dir": "/run/cloud-init/tmp"}] == calls def test_mkstemp_default_non_root(self): """mkstemp creates secure tempfile under /tmp for the unprivileged.""" @@ -91,8 +93,8 @@ def fake_mkstemp(*args, **kwargs): }, mkstemp, ) - self.assertEqual("/fake/return/path", retval) - self.assertEqual([{"dir": self.prefix}], calls) + assert "/fake/return/path" == retval + assert [{"dir": self.prefix}] == calls def test_mkstemp_default_root(self): """mkstemp creates a secure tempfile in /run/cloud-init for root.""" @@ -111,13 +113,13 @@ def fake_mkstemp(*args, **kwargs): }, mkstemp, ) - self.assertEqual("/fake/return/path", retval) - self.assertEqual([{"dir": "/run/cloud-init/tmp"}], calls) + assert "/fake/return/path" == retval + assert [{"dir": "/run/cloud-init/tmp"}] == calls def test_tempdir_error_suppression(self): """test tempdir suppresses errors during directory removal.""" - with self.assertRaises(OSError): + with pytest.raises(OSError): with tempdir(prefix="cloud-init-dhcp-") as tdir: os.rmdir(tdir) # As a result, the directory is already gone, diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py index 9171a58adc4..415748d4edf 100644 --- a/tests/unittests/test_templating.py +++ b/tests/unittests/test_templating.py @@ -4,7 +4,9 @@ # # This file is part of cloud-init. See LICENSE file for license information. +import logging import textwrap +from unittest import mock import pytest @@ -14,9 +16,7 @@ from tests.unittests import helpers as test_helpers -class TestTemplates(test_helpers.CiTestCase): - - with_logs = True +class TestTemplates: jinja_utf8 = b"It\xe2\x80\x99s not ascii, {{name}}\n" jinja_utf8_rbob = b"It\xe2\x80\x99s not ascii, bob\n".decode("utf-8") @@ -45,18 +45,18 @@ def test_render_basic(self): """ ) out_data = templater.basic_render(in_data, {"b": 2}) - self.assertEqual(expected_data.strip(), out_data) + assert expected_data.strip() == out_data def test_render_jinja(self): blob = """## template:jinja {{a}},{{b}}""" c = templater.render_string(blob, {"a": 1, "b": 2}) - self.assertEqual("1,2", c) + assert "1,2" == c def test_render_default(self): blob = """$a,$b""" c = templater.render_string(blob, {"a": 1, "b": 2}) - self.assertEqual("1,2", c) + assert "1,2" == c def test_render_basic_deeper(self): hn = "myfoohost.yahoo.com" @@ -68,21 +68,21 @@ def test_render_basic_deeper(self): }, } out_data = templater.render_string(in_data, params) - self.assertEqual(expected_data, out_data) + assert expected_data == out_data def test_render_basic_no_parens(self): hn = "myfoohost" in_data = "h=$hostname\nc=d\n" expected_data = "h=%s\nc=d\n" % hn out_data = templater.basic_render(in_data, {"hostname": hn}) - self.assertEqual(expected_data, out_data) + assert expected_data == out_data def test_render_basic_parens(self): hn = "myfoohost" in_data = "h = ${hostname}\nc=d\n" expected_data = "h = %s\nc=d\n" % hn out_data = templater.basic_render(in_data, {"hostname": hn}) - self.assertEqual(expected_data, out_data) + assert expected_data == out_data def test_render_basic2(self): mirror = "mymirror" @@ -96,30 +96,27 @@ def test_render_basic2(self): out_data = templater.basic_render( in_data, {"mirror": mirror, "codename": codename} ) - self.assertEqual(ex_data, out_data) + assert ex_data == out_data def test_jinja_nonascii_render_to_string(self): """Test jinja render_to_string with non-ascii content.""" - self.assertEqual( + assert ( templater.render_string( self.add_header("jinja", self.jinja_utf8), {"name": "bob"} - ), - self.jinja_utf8_rbob, + ) + == self.jinja_utf8_rbob ) def test_jinja_nonascii_render_undefined_variables_to_default_py3(self): """Test py3 jinja render_to_string with undefined variable default.""" - self.assertEqual( - templater.render_string( - self.add_header("jinja", self.jinja_utf8), {} - ), - self.jinja_utf8_rbob.replace("bob", "CI_MISSING_JINJA_VAR/name"), - ) + assert templater.render_string( + self.add_header("jinja", self.jinja_utf8), {} + ) == self.jinja_utf8_rbob.replace("bob", "CI_MISSING_JINJA_VAR/name") - def test_jinja_nonascii_render_to_file(self): + def test_jinja_nonascii_render_to_file(self, tmp_path): """Test jinja render_to_file of a filename with non-ascii content.""" - tmpl_fn = self.tmp_path("j-render-to-file.template") - out_fn = self.tmp_path("j-render-to-file.out") + tmpl_fn = str(tmp_path / "j-render-to-file.template") + out_fn = str(tmp_path / "j-render-to-file.out") write_file( filename=tmpl_fn, omode="wb", @@ -127,21 +124,21 @@ def test_jinja_nonascii_render_to_file(self): ) templater.render_to_file(tmpl_fn, out_fn, {"name": "bob"}) result = load_binary_file(out_fn).decode("utf-8") - self.assertEqual(result, self.jinja_utf8_rbob) + assert result == self.jinja_utf8_rbob - def test_jinja_nonascii_render_from_file(self): + def test_jinja_nonascii_render_from_file(self, tmp_path): """Test jinja render_from_file with non-ascii content.""" - tmpl_fn = self.tmp_path("j-render-from-file.template") + tmpl_fn = str(tmp_path / "j-render-from-file.template") write_file( tmpl_fn, omode="wb", content=self.add_header("jinja", self.jinja_utf8).encode("utf-8"), ) result = templater.render_from_file(tmpl_fn, {"name": "bob"}) - self.assertEqual(result, self.jinja_utf8_rbob) + assert result == self.jinja_utf8_rbob @test_helpers.skipIfJinja() - def test_jinja_warns_on_missing_dep_and_uses_basic_renderer(self): + def test_jinja_warns_on_missing_dep_and_uses_basic_renderer(self, caplog): """Test jinja render_from_file will fallback to basic renderer.""" tmpl_fn = self.tmp_path("j-render-from-file.template") write_file( @@ -150,12 +147,13 @@ def test_jinja_warns_on_missing_dep_and_uses_basic_renderer(self): content=self.add_header("jinja", self.jinja_utf8).encode("utf-8"), ) result = templater.render_from_file(tmpl_fn, {"name": "bob"}) - self.assertEqual(result, self.jinja_utf8.decode()) - self.assertIn( - "WARNING: Jinja not available as the selected renderer for desired" + assert result == self.jinja_utf8.decode() + assert ( + mock.ANY, + logging.WARNING, + "Jinja not available as the selected renderer for desired" " template, reverting to the basic renderer.", - self.logs.getvalue(), - ) + ) in caplog.record_tuples def test_jinja_do_extension_render_to_string(self): """Test jinja render_to_string using do extension.""" @@ -164,11 +162,11 @@ def test_jinja_do_extension_render_to_string(self): "{% set r = [] %} {% set input = [1,2,3] %} " "{% for i in input %} {% do r.append(i) %} {% endfor %} {{r}}" ) - self.assertEqual( + assert ( templater.render_string( self.add_header("jinja", jinja_template), {} - ).strip(), - expected_result, + ).strip() + == expected_result ) From f1f28d0a07d4fe9e05aba2db68fc28f415778bb1 Mon Sep 17 00:00:00 2001 From: Amirhossein Shaerpour <87924605+shaerpour@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:37:36 +0330 Subject: [PATCH 082/150] doc: change `jammy` to `noble` (#6398) --- doc/rtd/howto/launch_libvirt.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/rtd/howto/launch_libvirt.rst b/doc/rtd/howto/launch_libvirt.rst index 3b481ed267f..fa05b1bb43e 100644 --- a/doc/rtd/howto/launch_libvirt.rst +++ b/doc/rtd/howto/launch_libvirt.rst @@ -21,8 +21,8 @@ Create an instance .. code-block:: shell-session virt-install --name cloud-init-001 --memory 4000 --noreboot \ - --os-variant detect=on,name=ubuntujammy \ - --disk=size=10,backing_store="$(pwd)/jammy-server-cloudimg-amd64.img" \ + --os-variant detect=on,name=ubuntunoble \ + --disk=size=10,backing_store="$(pwd)/noble-server-cloudimg-amd64.img" \ --cloud-init user-data="$(pwd)/user-data,meta-data=$(pwd)/meta-data,network-config=$(pwd)/network-config" .. LINKS From 9224688458744806c64d3fa84ef41d6840303c65 Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Tue, 19 Aug 2025 15:52:35 -0600 Subject: [PATCH 083/150] chore(ci): bump versions for black, ruff, and mypy (#6406) --- .github/workflows/check_format.yml | 2 +- cloudinit/cmd/main.py | 4 ++-- cloudinit/config/cc_ca_certs.py | 2 ++ cloudinit/net/ephemeral.py | 3 +-- cloudinit/sources/__init__.py | 6 +++--- cloudinit/subp.py | 2 +- cloudinit/url_helper.py | 2 +- pyproject.toml | 2 +- tests/unittests/sources/test_oracle.py | 12 ++++++------ tools/validate-yaml.py | 3 +-- tox.ini | 6 +++--- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index 6e9b0bbc62f..3b77b2371db 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -19,7 +19,7 @@ jobs: matrix: env: [ruff, mypy, pylint, black, isort] name: Check ${{ matrix.env }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: "Checkout #1" uses: actions/checkout@v4 diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index d8d1d80c6f6..f2323639b25 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -345,7 +345,7 @@ def _should_bring_up_interfaces(init, args): def _should_wait_via_user_data( - raw_config: Optional[Union[str, bytes]] + raw_config: Optional[Union[str, bytes]], ) -> Tuple[bool, Reason]: """Determine if our cloud-config requires us to wait @@ -767,7 +767,7 @@ def main_modules(action_name, args): util.logexc(LOG, msg) print_exc(msg) if not args.force: - return [(msg)] + return [msg] _maybe_persist_instance_data(init) # Stage 3 mods = Modules(init, extract_fns(args), reporter=args.reporter) diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py index 4b89977fd1e..7344a9334e8 100644 --- a/cloudinit/config/cc_ca_certs.py +++ b/cloudinit/config/cc_ca_certs.py @@ -259,6 +259,8 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: " Ignoring ca-certs." ) ca_cert_cfg = cfg.get("ca_certs", cfg.get("ca-certs")) + if not isinstance(ca_cert_cfg, dict): + raise TypeError("unexpected type: {ca_cert_cfg}") distro_cfg = _distro_ca_certs_configs(cloud.distro.name) # If there is a remove_defaults option set to true, disable the system diff --git a/cloudinit/net/ephemeral.py b/cloudinit/net/ephemeral.py index 08e6086e718..ad137e56841 100644 --- a/cloudinit/net/ephemeral.py +++ b/cloudinit/net/ephemeral.py @@ -1,7 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -"""Module for ephemeral network context managers -""" +"""Module for ephemeral network context managers""" import contextlib import logging from functools import partial diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index b42ec3faef7..820c09257af 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -26,9 +26,9 @@ net, performance, type_utils, + user_data, + util, ) -from cloudinit import user_data as ud -from cloudinit import util from cloudinit.atomic_helper import write_json from cloudinit.distros import Distro from cloudinit.event import EventScope, EventType @@ -354,7 +354,7 @@ def __init__(self, sys_cfg, distro: Distro, paths: Paths, ud_proc=None): self.ds_cfg = {} if not ud_proc: - self.ud_proc = ud.UserDataProcessor(self.paths) + self.ud_proc = user_data.UserDataProcessor(self.paths) else: self.ud_proc = ud_proc diff --git a/cloudinit/subp.py b/cloudinit/subp.py index eebf009d4f0..12fba876021 100644 --- a/cloudinit/subp.py +++ b/cloudinit/subp.py @@ -258,7 +258,7 @@ def subp( ] try: with performance.Timed( - "Running {}".format(logstring if logstring else args) + "Running {!r}".format(logstring if logstring else args) ): sp = subprocess.Popen( bytes_args, diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py index 9734bdbd8b3..dbaaac61eae 100644 --- a/cloudinit/url_helper.py +++ b/cloudinit/url_helper.py @@ -85,7 +85,7 @@ def ftp_get_return_code_from_exception(exc) -> int: } code = ftp_error_codes.get(type(exc)) # pyright: ignore if not code: - if isinstance(exc, OSError): + if isinstance(exc, OSError) and exc.errno: code = exc.errno else: LOG.warning( diff --git a/pyproject.toml b/pyproject.toml index 143154779e3..3bde817967d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,7 +199,7 @@ module = [ check_untyped_defs = false [tool.ruff] -target-version = "py37" +target-version = "py38" line-length = 79 # E, W, and F make up the entirety of default flake8 lint.select = [ diff --git a/tests/unittests/sources/test_oracle.py b/tests/unittests/sources/test_oracle.py index 1a61510a3b2..761e64fbd02 100644 --- a/tests/unittests/sources/test_oracle.py +++ b/tests/unittests/sources/test_oracle.py @@ -1371,12 +1371,12 @@ def assert_in_context_manager(**kwargs): ), ): # datasource fails/exits if ephemeral dhcp setup fails - with ( - pytest.raises(Exception) - if ephemeral_dhcp_setup_raises_exception - else test_helpers.does_not_raise() - ): - assert oracle_ds._check_and_get_data() + if ephemeral_dhcp_setup_raises_exception: + with pytest.raises(Exception): + assert oracle_ds._check_and_get_data() + else: + with test_helpers.does_not_raise(): + assert oracle_ds._check_and_get_data() if perform_dhcp_setup: assert [ diff --git a/tools/validate-yaml.py b/tools/validate-yaml.py index fa90edf1a98..2219e657b80 100755 --- a/tools/validate-yaml.py +++ b/tools/validate-yaml.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 -"""Try to read a YAML file and report any errors. -""" +"""Try to read a YAML file and report any errors.""" import sys import yaml diff --git a/tox.ini b/tox.ini index e7f3f6f14c2..48886fbca97 100644 --- a/tox.ini +++ b/tox.ini @@ -39,13 +39,13 @@ deps = [pinned_versions] deps = {[types]deps} - black==24.8.0 + black==25.1.0 hypothesis==6.111.0 hypothesis_jsonschema==0.23.1 isort==5.13.2 - mypy==1.11.1 + mypy==1.17.1 pylint==3.2.6 - ruff==0.6.4 + ruff==0.12.9 [latest_versions] deps = From a90e652527cffe2e125809dc812e1ee694ebdcf1 Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Tue, 19 Aug 2025 16:26:58 -0600 Subject: [PATCH 084/150] chore(ci): fix cloud-init test failures from new pylint (#6407) --- cloudinit/analyze/dump.py | 4 ++++ cloudinit/cmd/main.py | 6 +++++- cloudinit/config/cc_disk_setup.py | 2 +- cloudinit/config/cc_mounts.py | 8 +++++--- cloudinit/log/loggers.py | 2 +- cloudinit/net/netplan.py | 9 ++++++--- cloudinit/net/networkd.py | 2 +- cloudinit/net/sysconfig.py | 2 +- cloudinit/sources/DataSourceAliYun.py | 3 --- cloudinit/sources/DataSourceAzure.py | 2 +- cloudinit/sources/DataSourceCloudStack.py | 1 + cloudinit/sources/DataSourceOracle.py | 4 +++- cloudinit/url_helper.py | 6 ++++-- tools/test_tools.py | 2 +- tox.ini | 1 + 15 files changed, 35 insertions(+), 19 deletions(-) diff --git a/cloudinit/analyze/dump.py b/cloudinit/analyze/dump.py index dd1b9c32838..5837852b32b 100644 --- a/cloudinit/analyze/dump.py +++ b/cloudinit/analyze/dump.py @@ -179,6 +179,10 @@ def dump_events( data = rawdata.splitlines() elif cisource is not None: data = cisource.readlines() + else: + raise ValueError( + "Either cisource or rawdata parameters must have a value" + ) for line in data: for match in CI_EVENT_MATCHES: diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index f2323639b25..05b9336d2f7 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -80,6 +80,10 @@ class SubcommandAwareArgumentParser(argparse.ArgumentParser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._raw_args = None + def parse_args(self, args=None, namespace=None): """Override parse_args to store raw arguments for error handling.""" self._raw_args = args @@ -91,7 +95,7 @@ def error(self, message): # Scan for the first valid subcommand - if not hasattr(self, "_raw_args"): + if not self._raw_args: self._raw_args = sys.argv[1:] subcommand = None if self._raw_args: diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py index 058d27ea97e..f61622720cc 100644 --- a/cloudinit/config/cc_disk_setup.py +++ b/cloudinit/config/cc_disk_setup.py @@ -1112,7 +1112,7 @@ def exec_mkpart_gpt_sgdisk(device, layout): def exec_mkpart_gpt_sfdisk(device, layout): cmd = "" # Promote partition types to GPT partition GUIDs - for partition_type, (start, end) in layout: + for partition_type, (_, end) in layout: partition_type = str(partition_type).ljust(4, "0") if len(partition_type) == 4 and partition_type in sgdisk_to_gpt_id: partition_type = sgdisk_to_gpt_id[partition_type] diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py index c3998e578be..1b4230c51d0 100644 --- a/cloudinit/config/cc_mounts.py +++ b/cloudinit/config/cc_mounts.py @@ -514,18 +514,20 @@ def mount_if_needed( subp.subp(["systemctl", "daemon-reload"]) -def cleanup_fstab(ds_remove_entries: list = []) -> None: +def cleanup_fstab(ds_remove_entry: Optional[str] = None) -> None: if not os.path.exists(FSTAB_PATH): return - base_entry = [MNT_COMMENT] + remove_entries = [MNT_COMMENT] + if ds_remove_entry: + remove_entries.append(ds_remove_entry) with open(FSTAB_PATH, "r") as f: lines = f.readlines() new_lines = [] changed = False for line in lines: - if all(entry in line for entry in [*base_entry, *ds_remove_entries]): + if all(entry in line for entry in remove_entries): changed = True continue new_lines.append(line) diff --git a/cloudinit/log/loggers.py b/cloudinit/log/loggers.py index fd83c994c88..a3e5a78c7f5 100644 --- a/cloudinit/log/loggers.py +++ b/cloudinit/log/loggers.py @@ -8,7 +8,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -import collections.abc +import collections.abc # pylint: disable=import-error import copy import io import logging diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index ae90db4ba0b..96341a5a506 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -431,6 +431,7 @@ def _net_setup_link(self, run=False): # net_setup_link on a device that no longer exists. When this happens, # we don't know what the device was renamed to, so re-gather the # entire list of devices and try again. + last_exception: Optional[Exception] for _ in range(5): try: for iface in get_devicelist(): @@ -438,10 +439,11 @@ def _net_setup_link(self, run=False): subp.subp( setup_lnk + [SYS_CLASS_NET + iface], capture=True ) + last_exception = None break except subp.ProcessExecutionError as e: last_exception = e - else: + if last_exception: raise RuntimeError( "'udevadm test-builtin net_setup_link' unable to run " "successfully for all devices." @@ -527,8 +529,9 @@ def _render_content(self, network_state: NetworkState) -> str: bridge_ports = ifcfg.get("bridge_ports") if bridge_ports is None: LOG.warning( - "Invalid config. The key", - f"'bridge_ports' is required in {config}.", + "Invalid config. The key" + "'bridge_ports' is required in %s.", + config, ) continue ports = sorted(copy.copy(bridge_ports)) diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py index 777490e6f6b..ae1e83c2c6b 100644 --- a/cloudinit/net/networkd.py +++ b/cloudinit/net/networkd.py @@ -620,7 +620,7 @@ def render_bonds(self, ns: NetworkState) -> dict: "lacp-rate": ("LACPTransmitRate", str), "learn-packet-interval": ("LearnPacketIntervalSec", str), "mii-monitor-interval": ("MIIMonitorSec", lambda v: f"{v}ms"), - "min-links": ("MinLinks", lambda v: str(v)), + "min-links": ("MinLinks", str), "mode": ("Mode", str), "packets-per-slave": ("PacketsPerSlave", str), "primary-reselect-policy": ("PrimaryReselectPolicy", str), diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 98e4ed9398d..5d45babdf8d 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -1056,7 +1056,7 @@ def _render_sysconfig( cls._render_bridge_interfaces(network_state, iface_contents, flavor) cls._render_ib_interfaces(network_state, iface_contents, flavor) contents = {} - for iface_name, iface_cfg in iface_contents.items(): + for _, iface_cfg in iface_contents.items(): if iface_cfg or iface_cfg.children: contents[iface_cfg.path] = iface_cfg.to_string() for iface_cfg in iface_cfg.children: diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py index 286ed2af9a1..b9a7a995b42 100644 --- a/cloudinit/sources/DataSourceAliYun.py +++ b/cloudinit/sources/DataSourceAliYun.py @@ -49,9 +49,6 @@ def __init__(self, sys_cfg, distro, paths): self.default_update_events = copy.deepcopy(self.default_update_events) self.default_update_events[EventScope.NETWORK].add(EventType.BOOT) - def _unpickle(self, ci_pkl_version: int) -> None: - super()._unpickle(ci_pkl_version) - def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): hostname = self.metadata.get("hostname") is_default = False diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 80ac60b77b0..06872659300 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -1676,7 +1676,7 @@ def _cleanup_resourcedisk_fstab(self): by cloud-init i.e. lines containing "/dev/disk/cloud/azure_resource" and cloudconfig comment. """ - cc_mounts.cleanup_fstab([RESOURCE_DISK_PATH]) + cc_mounts.cleanup_fstab(RESOURCE_DISK_PATH) def clean(self): # Azure-specific cleanup logic for "cloud-init clean -c datasource" diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py index 80dcfb13fdf..789245c0e5f 100644 --- a/cloudinit/sources/DataSourceCloudStack.py +++ b/cloudinit/sources/DataSourceCloudStack.py @@ -94,6 +94,7 @@ def __init__(self, sys_cfg, distro, paths): # http:///latest/ self.api_ver = "latest" self.cfg = {} + self.vr_addr = None def _get_domainname(self): """ diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py index aa62aa2d2aa..9f5eb254825 100644 --- a/cloudinit/sources/DataSourceOracle.py +++ b/cloudinit/sources/DataSourceOracle.py @@ -470,7 +470,7 @@ def read_opc_metadata( fetch_vnics_data: bool = False, max_wait=DataSourceOracle.url_max_wait, timeout=DataSourceOracle.url_timeout, - metadata_patterns: List[str] = [IPV4_METADATA_PATTERN], + metadata_patterns: Optional[List[str]] = None, ) -> Optional[ReadOpcMetadataResponse]: """ Fetch metadata from the /opc/ routes from the IMDS. @@ -487,6 +487,8 @@ def read_opc_metadata( This allows for later determining if v1 or v2 endppoint was used and whether the IMDS was reached via IPv4 or IPv6. """ + if metadata_patterns is None: + metadata_patterns = [IPV4_METADATA_PATTERN] urls = [ metadata_pattern.format(version=version, path="instance") for version in [2, 1] diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py index dbaaac61eae..bae99b0322d 100644 --- a/cloudinit/url_helper.py +++ b/cloudinit/url_helper.py @@ -387,13 +387,15 @@ def _get_retry_after(retry_after: str) -> float: """ try: to_wait = float(retry_after) - except ValueError: + except ValueError as exc: # Translate a date such as "Fri, 31 Dec 1999 23:59:59 GMT" # into seconds to wait try: time_tuple = parsedate(retry_after) if not time_tuple: - raise ValueError("Failed to parse Retry-After header value") + raise ValueError( + "Failed to parse Retry-After header value" + ) from exc to_wait = float(time.mktime(time_tuple) - time.time()) except ValueError: LOG.info( diff --git a/tools/test_tools.py b/tools/test_tools.py index e744e1d121c..694aed61427 100644 --- a/tools/test_tools.py +++ b/tools/test_tools.py @@ -7,7 +7,7 @@ import setuptools -from setup_utils import version_to_pep440 +from setup_utils import version_to_pep440 # pylint: disable=import-error try: validate_version = setuptools.dist.Distribution._validate_version # type: ignore # noqa: E501 diff --git a/tox.ini b/tox.ini index 48886fbca97..7a1a0d0696c 100644 --- a/tox.ini +++ b/tox.ini @@ -227,6 +227,7 @@ commands = {envpython} -m mypy {posargs:cloudinit/ tests/ tools/} [testenv:tip-pylint] deps = {[latest_versions]deps} + -r{toxinidir}/integration-requirements.txt {[testenv]deps} commands = {envpython} -m pylint {posargs:.} From 885843a5262221a3a1ea3b49ea9f780afc4b29b8 Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Wed, 20 Aug 2025 08:04:00 -0600 Subject: [PATCH 085/150] chore(ci): bump pylint version, fix test failures (#6408) --- tests/integration_tests/clouds.py | 1 + tests/integration_tests/conftest.py | 4 ++-- tests/unittests/analyze/test_boot.py | 2 +- tests/unittests/config/test_cc_resizefs.py | 3 --- tests/unittests/net/test_net_rendering.py | 4 +++- tests/unittests/sources/test_cloudcix.py | 1 + tests/unittests/sources/test_cloudstack.py | 1 + tests/unittests/sources/test_digitalocean.py | 1 + tests/unittests/sources/test_gce.py | 1 + tests/unittests/sources/test_ibmcloud.py | 1 + tests/unittests/sources/test_opennebula.py | 1 + tests/unittests/sources/test_openstack.py | 2 -- tests/unittests/sources/test_oracle.py | 4 +++- tests/unittests/sources/test_ovf.py | 1 + tests/unittests/sources/test_scaleway.py | 1 + tests/unittests/sources/test_smartos.py | 17 +++++++++++------ tests/unittests/test_dmi.py | 4 +++- tests/unittests/test_templating.py | 6 ++++-- tox.ini | 11 +++++++---- 19 files changed, 43 insertions(+), 23 deletions(-) diff --git a/tests/integration_tests/clouds.py b/tests/integration_tests/clouds.py index a93a2d2da3e..e72213a1002 100644 --- a/tests/integration_tests/clouds.py +++ b/tests/integration_tests/clouds.py @@ -334,6 +334,7 @@ def _mount_source(instance: LXDInstance): ).format(**format_variables) subp(command.split()) + # pylint: disable=assignment-from-none def _perform_launch( self, *, diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 194cda58d64..5a391b6a60f 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -145,11 +145,11 @@ def session_cloud( image_types = [member.value for member in ImageType.__members__.values()] try: image_type = ImageType(integration_settings.OS_IMAGE_TYPE) - except ValueError: + except ValueError as e: raise ValueError( f"{integration_settings.OS_IMAGE_TYPE} is an invalid OS_IMAGE_TYPE" f" specified in settings. Must be one of {image_types}" - ) + ) from e cloud: IntegrationCloud = platforms[integration_settings.PLATFORM]( reaper=reaper, image_type=image_type diff --git a/tests/unittests/analyze/test_boot.py b/tests/unittests/analyze/test_boot.py index 18911c96ff9..ffe147e5110 100644 --- a/tests/unittests/analyze/test_boot.py +++ b/tests/unittests/analyze/test_boot.py @@ -24,7 +24,7 @@ def test_blank_distro(self, m_subp): @mock.patch("cloudinit.subp.subp") @mock.patch("cloudinit.util.is_FreeBSD", return_value=True) def test_freebsd_gentoo_cant_find(self, m_is_FreeBSD, m_subp): - err_code == dist_check_timestamp() + assert err_code == dist_check_timestamp() @mock.patch("cloudinit.subp.subp", return_value=(0, 1)) def test_subp_fails(self, m_subp): diff --git a/tests/unittests/config/test_cc_resizefs.py b/tests/unittests/config/test_cc_resizefs.py index 612f76f772d..937df1815d5 100644 --- a/tests/unittests/config/test_cc_resizefs.py +++ b/tests/unittests/config/test_cc_resizefs.py @@ -36,9 +36,6 @@ class TestResizefs: - def setUp(self): - super(TestResizefs, self).setUp() - self.name = "resizefs" @mock.patch("cloudinit.subp.subp") def test_skip_ufs_resize(self, m_subp): diff --git a/tests/unittests/net/test_net_rendering.py b/tests/unittests/net/test_net_rendering.py index 72fe3b1b167..5ef8171817d 100644 --- a/tests/unittests/net/test_net_rendering.py +++ b/tests/unittests/net/test_net_rendering.py @@ -125,4 +125,6 @@ def test_convert(test_name, renderers, tmp_path): if Renderer.NetworkManager in renderers: _check_network_manager(network_state, tmp_path) if Renderer.Networkd in renderers: - _check_networkd_renderer(network_state, tmp_path) + _check_networkd_renderer( # pylint: disable=E1120 + network_state, tmp_path + ) diff --git a/tests/unittests/sources/test_cloudcix.py b/tests/unittests/sources/test_cloudcix.py index 531e6022f9f..9d57677d470 100644 --- a/tests/unittests/sources/test_cloudcix.py +++ b/tests/unittests/sources/test_cloudcix.py @@ -1,4 +1,5 @@ # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import json from unittest import mock diff --git a/tests/unittests/sources/test_cloudstack.py b/tests/unittests/sources/test_cloudstack.py index 990c8a7f7aa..eb49a7b3269 100644 --- a/tests/unittests/sources/test_cloudstack.py +++ b/tests/unittests/sources/test_cloudstack.py @@ -1,4 +1,5 @@ # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init from socket import gaierror from textwrap import dedent diff --git a/tests/unittests/sources/test_digitalocean.py b/tests/unittests/sources/test_digitalocean.py index 0ef2e51a96a..b5bc526ed37 100644 --- a/tests/unittests/sources/test_digitalocean.py +++ b/tests/unittests/sources/test_digitalocean.py @@ -5,6 +5,7 @@ # Author: Scott Moser # # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import json diff --git a/tests/unittests/sources/test_gce.py b/tests/unittests/sources/test_gce.py index d1e60472eaf..e99414996aa 100644 --- a/tests/unittests/sources/test_gce.py +++ b/tests/unittests/sources/test_gce.py @@ -3,6 +3,7 @@ # Author: Vaidas Jablonskis # # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import datetime import json diff --git a/tests/unittests/sources/test_ibmcloud.py b/tests/unittests/sources/test_ibmcloud.py index 2589871a5f8..f6b81908edf 100644 --- a/tests/unittests/sources/test_ibmcloud.py +++ b/tests/unittests/sources/test_ibmcloud.py @@ -1,4 +1,5 @@ # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import base64 import copy diff --git a/tests/unittests/sources/test_opennebula.py b/tests/unittests/sources/test_opennebula.py index 317a842bc89..c6f4066abbe 100644 --- a/tests/unittests/sources/test_opennebula.py +++ b/tests/unittests/sources/test_opennebula.py @@ -1,4 +1,5 @@ # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import os import pwd diff --git a/tests/unittests/sources/test_openstack.py b/tests/unittests/sources/test_openstack.py index f26c70f84fd..85582716cd8 100644 --- a/tests/unittests/sources/test_openstack.py +++ b/tests/unittests/sources/test_openstack.py @@ -557,8 +557,6 @@ def test_vd_load_dict_ci_list(self): @test_helpers.mock.patch(MOCK_PATH + "util.is_x86") class TestDetectOpenStack: - def setUp(self): - self.tmp = self.tmp_dir() @pytest.fixture def fake_ds(self, paths) -> ds.DataSourceOpenStack: diff --git a/tests/unittests/sources/test_oracle.py b/tests/unittests/sources/test_oracle.py index 761e64fbd02..14d7ad29a21 100644 --- a/tests/unittests/sources/test_oracle.py +++ b/tests/unittests/sources/test_oracle.py @@ -1340,7 +1340,9 @@ def exit_context_manager(*args): if ephemeral_dhcp_setup_raises_exception: def raise_exception(**kwargs): - raise Exception("Failed to setup ephemeral network") + raise Exception( # pylint: disable=W0719 + "Failed to setup ephemeral network" + ) m_ephemeral_network.side_effect = raise_exception else: diff --git a/tests/unittests/sources/test_ovf.py b/tests/unittests/sources/test_ovf.py index c5392f9ae76..5b771df1220 100644 --- a/tests/unittests/sources/test_ovf.py +++ b/tests/unittests/sources/test_ovf.py @@ -3,6 +3,7 @@ # Author: Scott Moser # # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import base64 import os diff --git a/tests/unittests/sources/test_scaleway.py b/tests/unittests/sources/test_scaleway.py index cc99ba742a6..8a0736443e5 100644 --- a/tests/unittests/sources/test_scaleway.py +++ b/tests/unittests/sources/test_scaleway.py @@ -1,4 +1,5 @@ # This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import json import socket diff --git a/tests/unittests/sources/test_smartos.py b/tests/unittests/sources/test_smartos.py index e73b7c4d37e..1f4418eba91 100644 --- a/tests/unittests/sources/test_smartos.py +++ b/tests/unittests/sources/test_smartos.py @@ -791,7 +791,7 @@ def joyent_metadata(mocker, m_serial): "request_id", "metadata_value", "response_parts", - "meta_source_data", + "metasource_data", "metasource_data_len", ], defaults=(None, None, None, None, None, None), @@ -810,12 +810,15 @@ def joyent_metadata(mocker, m_serial): def make_response(): payloadstr = "" - if "payload" in res.response_parts: - payloadstr = " {0}".format(res.response_parts["payload"]) + if "payload" in res.response_parts: # pylint: disable=E1135 + payloadstr = " {0}".format( + res.response_parts["payload"] # pylint: disable=E1146,E1136 + ) return ( "V2 {length} {crc} {request_id} " "{command}{payloadstr}\n".format( - payloadstr=payloadstr, **res.response_parts + payloadstr=payloadstr, + **res.response_parts # pylint: disable=E1134 ).encode("ascii") ) @@ -825,8 +828,10 @@ def read_response(length): if not res.metasource_data: res.metasource_data = make_response() res.metasource_data_len = len(res.metasource_data) - resp = res.metasource_data[:length] - res.metasource_data = res.metasource_data[length:] + resp = res.metasource_data[:length] # pylint: disable=E1136 + res.metasource_data = res.metasource_data[ # pylint: disable=E1136 + length: + ] return resp res.serial.read.side_effect = read_response diff --git a/tests/unittests/test_dmi.py b/tests/unittests/test_dmi.py index e8eeb372a17..c951c93f1cf 100644 --- a/tests/unittests/test_dmi.py +++ b/tests/unittests/test_dmi.py @@ -1,3 +1,5 @@ +# This file is part of cloud-init. See LICENSE file for license information. +# pylint: disable=attribute-defined-outside-init import os from unittest import mock @@ -97,7 +99,7 @@ def test_dmidecode_used_if_no_sysfs_file_on_disk(self, mocker): "x-version", "x86_64", ) - expected_dmi_value == dmi.read_dmi_data("use-dmidecode") + assert expected_dmi_value == dmi.read_dmi_data("use-dmidecode") def test_dmidecode_not_used_on_arm(self, mocker): mocker.patch("cloudinit.dmi.DMIDECODE_TO_KERNEL", {}) diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py index 415748d4edf..3b14e8a3f25 100644 --- a/tests/unittests/test_templating.py +++ b/tests/unittests/test_templating.py @@ -138,9 +138,11 @@ def test_jinja_nonascii_render_from_file(self, tmp_path): assert result == self.jinja_utf8_rbob @test_helpers.skipIfJinja() - def test_jinja_warns_on_missing_dep_and_uses_basic_renderer(self, caplog): + def test_jinja_warns_on_missing_dep_and_uses_basic_renderer( + self, caplog, tmp_path + ): """Test jinja render_from_file will fallback to basic renderer.""" - tmpl_fn = self.tmp_path("j-render-from-file.template") + tmpl_fn = tmp_path("j-render-from-file.template") write_file( tmpl_fn, omode="wb", diff --git a/tox.ini b/tox.ini index 7a1a0d0696c..b0ecb348a69 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ deps = hypothesis_jsonschema==0.23.1 isort==5.13.2 mypy==1.17.1 - pylint==3.2.6 + pylint==3.3.8 ruff==0.12.9 [latest_versions] @@ -69,8 +69,11 @@ deps = {[pinned_versions]deps} commands = {envpython} -m ruff check {posargs:.} [testenv:pylint] -deps = {[pinned_versions]deps} -commands = {envpython} -m pylint {posargs:.} +deps = + -r{toxinidir}/integration-requirements.txt + {[pinned_versions]deps} + {[testenv]deps} +commands = {envpython} -m pylint {posargs:cloudinit/ tests/ tools/} [testenv:black] deps = {[pinned_versions]deps} @@ -229,7 +232,7 @@ commands = {envpython} -m mypy {posargs:cloudinit/ tests/ tools/} deps = {[latest_versions]deps} -r{toxinidir}/integration-requirements.txt {[testenv]deps} -commands = {envpython} -m pylint {posargs:.} +commands = {envpython} -m pylint {posargs:cloudinit/ tests/ tools/} [testenv:tip-black] deps = {[latest_versions]deps} From 64afa5343c745ea4214eb059a5a6ebb502d0c7ae Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 20 Aug 2025 16:39:44 +0200 Subject: [PATCH 086/150] test: drop citestcase from net/test_dhcp.py (#6401) --- tests/unittests/net/test_dhcp.py | 308 ++++++++++++++----------------- 1 file changed, 135 insertions(+), 173 deletions(-) diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py index 708566422d9..aaaf92fc583 100644 --- a/tests/unittests/net/test_dhcp.py +++ b/tests/unittests/net/test_dhcp.py @@ -27,7 +27,7 @@ from cloudinit.subp import SubpResult from cloudinit.util import ensure_file, load_binary_file, subp, write_file from tests.unittests.helpers import ( - CiTestCase, + assert_count_equal, example_netdev, mock, populate_dir, @@ -81,30 +81,32 @@ def test_find_server_address_when_present( ) +@pytest.fixture +def isc_dh_cli(tmp_path): + cli = IscDhclient() + cli.lease_file = str(tmp_path / "leases") + return cli + + @pytest.mark.usefixtures("dhclient_exists") -class TestParseDHCPLeasesFile(CiTestCase): - def test_parse_empty_lease_file_errors(self): +class TestParseDHCPLeasesFile: + + def test_parse_empty_lease_file_errors(self, isc_dh_cli): """get_newest_lease errors when file content is empty.""" - client = IscDhclient() - client.lease_file = self.tmp_path("leases") - ensure_file(client.lease_file) - assert not client.get_newest_lease("eth0") + ensure_file(isc_dh_cli.lease_file) + assert not isc_dh_cli.get_newest_lease("eth0") - def test_parse_malformed_lease_file_content_errors(self): + def test_parse_malformed_lease_file_content_errors(self, isc_dh_cli): """IscDhclient.get_newest_lease errors when file content isn't dhcp leases. """ - client = IscDhclient() - client.lease_file = self.tmp_path("leases") - write_file(client.lease_file, "hi mom.") - assert not client.get_newest_lease("eth0") + write_file(isc_dh_cli.lease_file, "hi mom.") + assert not isc_dh_cli.get_newest_lease("eth0") - def test_parse_multiple_leases(self): + def test_parse_multiple_leases(self, isc_dh_cli): """IscDhclient().get_newest_lease returns the latest lease within. """ - client = IscDhclient() - client.lease_file = self.tmp_path("leases") content = dedent( """ lease { @@ -132,22 +134,22 @@ def test_parse_multiple_leases(self): "subnet-mask": "255.255.255.0", "routers": "192.168.2.1", } - write_file(client.lease_file, content) - got = client.get_newest_lease("eth0") - self.assertCountEqual(got, expected) + write_file(isc_dh_cli.lease_file, content) + got = isc_dh_cli.get_newest_lease("eth0") + assert_count_equal(got, expected) @pytest.mark.usefixtures("dhclient_exists") @pytest.mark.usefixtures("disable_netdev_info") -class TestDHCPRFC3442(CiTestCase): - def test_parse_lease_finds_rfc3442_classless_static_routes(self): +class TestDHCPRFC3442: + def test_parse_lease_finds_rfc3442_classless_static_routes( + self, isc_dh_cli + ): """IscDhclient().get_newest_lease() returns rfc3442-classless-static-routes. """ - client = IscDhclient() - client.lease_file = self.tmp_path("leases") write_file( - client.lease_file, + isc_dh_cli.lease_file, dedent( """ lease { @@ -171,15 +173,13 @@ def test_parse_lease_finds_rfc3442_classless_static_routes(self): "renew": "4 2017/07/27 18:02:30", "expire": "5 2017/07/28 07:08:15", } - self.assertCountEqual(expected, client.get_newest_lease("eth0")) + assert_count_equal(expected, isc_dh_cli.get_newest_lease("eth0")) - def test_parse_lease_finds_classless_static_routes(self): + def test_parse_lease_finds_classless_static_routes(self, isc_dh_cli): """ IscDhclient().get_newest_lease returns classless-static-routes for Centos lease format. """ - client = IscDhclient() - client.lease_file = self.tmp_path("leases") content = dedent( """ lease { @@ -202,8 +202,8 @@ def test_parse_lease_finds_classless_static_routes(self): "renew": "4 2017/07/27 18:02:30", "expire": "5 2017/07/28 07:08:15", } - write_file(client.lease_file, content) - self.assertCountEqual(expected, client.get_newest_lease("eth0")) + write_file(isc_dh_cli.lease_file, content) + assert_count_equal(expected, isc_dh_cli.get_newest_lease("eth0")) @mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network") @mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery") @@ -263,33 +263,29 @@ def test_obtain_centos_lease_parses_static_routes(self, m_maybe, m_ipv4): m_ipv4.assert_called_with(distro, **expected_kwargs) -class TestDHCPParseStaticRoutes(CiTestCase): - with_logs = True - +class TestDHCPParseStaticRoutes: def test_parse_static_routes_empty_string(self): - self.assertEqual([], IscDhclient.parse_static_routes("")) + assert [] == IscDhclient.parse_static_routes("") def test_parse_static_routes_invalid_input_returns_empty_list(self): rfc3442 = "32,169,254,169,254,130,56,248" - self.assertEqual([], IscDhclient.parse_static_routes(rfc3442)) + assert [] == IscDhclient.parse_static_routes(rfc3442) def test_parse_static_routes_bogus_width_returns_empty_list(self): rfc3442 = "33,169,254,169,254,130,56,248" - self.assertEqual([], IscDhclient.parse_static_routes(rfc3442)) + assert [] == IscDhclient.parse_static_routes(rfc3442) def test_parse_static_routes_single_ip(self): rfc3442 = "32,169,254,169,254,130,56,248,255" - self.assertEqual( - [("169.254.169.254/32", "130.56.248.255")], - IscDhclient.parse_static_routes(rfc3442), - ) + assert [ + ("169.254.169.254/32", "130.56.248.255") + ] == IscDhclient.parse_static_routes(rfc3442) def test_parse_static_routes_single_ip_handles_trailing_semicolon(self): rfc3442 = "32,169,254,169,254,130,56,248,255;" - self.assertEqual( - [("169.254.169.254/32", "130.56.248.255")], - IscDhclient.parse_static_routes(rfc3442), - ) + assert [ + ("169.254.169.254/32", "130.56.248.255") + ] == IscDhclient.parse_static_routes(rfc3442) def test_unknown_121(self): for unknown121 in [ @@ -304,35 +300,30 @@ def test_unknown_121(self): def test_parse_static_routes_default_route(self): rfc3442 = "0,130,56,240,1" - self.assertEqual( - [("0.0.0.0/0", "130.56.240.1")], - IscDhclient.parse_static_routes(rfc3442), - ) + assert [ + ("0.0.0.0/0", "130.56.240.1") + ] == IscDhclient.parse_static_routes(rfc3442) def test_unspecified_gateway(self): rfc3442 = "32,169,254,169,254,0,0,0,0" - self.assertEqual( - [("169.254.169.254/32", "0.0.0.0")], - IscDhclient.parse_static_routes(rfc3442), - ) + assert [ + ("169.254.169.254/32", "0.0.0.0") + ] == IscDhclient.parse_static_routes(rfc3442) def test_parse_static_routes_class_c_b_a(self): class_c = "24,192,168,74,192,168,0,4" class_b = "16,172,16,172,16,0,4" class_a = "8,10,10,0,0,4" rfc3442 = ",".join([class_c, class_b, class_a]) - self.assertEqual( - sorted( - [ - ("192.168.74.0/24", "192.168.0.4"), - ("172.16.0.0/16", "172.16.0.4"), - ("10.0.0.0/8", "10.0.0.4"), - ] - ), - sorted(IscDhclient.parse_static_routes(rfc3442)), - ) + assert sorted( + [ + ("192.168.74.0/24", "192.168.0.4"), + ("172.16.0.0/16", "172.16.0.4"), + ("10.0.0.0/8", "10.0.0.4"), + ] + ) == sorted(IscDhclient.parse_static_routes(rfc3442)) - def test_parse_static_routes_logs_error_truncated(self): + def test_parse_static_routes_logs_error_truncated(self, caplog): bad_rfc3442 = { "class_c": "24,169,254,169,10", "class_b": "16,172,16,10", @@ -341,52 +332,43 @@ def test_parse_static_routes_logs_error_truncated(self): "netlen": "33,0", } for rfc3442 in bad_rfc3442.values(): - self.assertEqual([], IscDhclient.parse_static_routes(rfc3442)) + assert [] == IscDhclient.parse_static_routes(rfc3442) - logs = self.logs.getvalue() - self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines())) + assert len(bad_rfc3442.keys()) == len(caplog.text.splitlines()) - def test_parse_static_routes_returns_valid_routes_until_parse_err(self): + def test_parse_static_routes_returns_valid_routes_until_parse_err( + self, caplog + ): class_c = "24,192,168,74,192,168,0,4" class_b = "16,172,16,172,16,0,4" class_a_error = "8,10,10,0,0" rfc3442 = ",".join([class_c, class_b, class_a_error]) - self.assertEqual( - sorted( - [ - ("192.168.74.0/24", "192.168.0.4"), - ("172.16.0.0/16", "172.16.0.4"), - ] - ), - sorted(IscDhclient.parse_static_routes(rfc3442)), - ) + assert sorted( + [ + ("192.168.74.0/24", "192.168.0.4"), + ("172.16.0.0/16", "172.16.0.4"), + ] + ) == sorted(IscDhclient.parse_static_routes(rfc3442)) - logs = self.logs.getvalue() - self.assertIn(rfc3442, logs.splitlines()[0]) + assert rfc3442 in caplog.text.splitlines()[0] def test_redhat_format(self): redhat_format = "24.191.168.128 192.168.128.1,0 192.168.128.1" - self.assertEqual( - sorted( - [ - ("191.168.128.0/24", "192.168.128.1"), - ("0.0.0.0/0", "192.168.128.1"), - ] - ), - sorted(IscDhclient.parse_static_routes(redhat_format)), - ) + assert sorted( + [ + ("191.168.128.0/24", "192.168.128.1"), + ("0.0.0.0/0", "192.168.128.1"), + ] + ) == sorted(IscDhclient.parse_static_routes(redhat_format)) def test_redhat_format_with_a_space_too_much_after_comma(self): redhat_format = "24.191.168.128 192.168.128.1, 0 192.168.128.1" - self.assertEqual( - sorted( - [ - ("191.168.128.0/24", "192.168.128.1"), - ("0.0.0.0/0", "192.168.128.1"), - ] - ), - sorted(IscDhclient.parse_static_routes(redhat_format)), - ) + assert sorted( + [ + ("191.168.128.0/24", "192.168.128.1"), + ("0.0.0.0/0", "192.168.128.1"), + ] + ) == sorted(IscDhclient.parse_static_routes(redhat_format)) class TestDHCPDiscoveryClean: @@ -696,7 +678,7 @@ def dhcp_log_func(interface, out, err): ) -class TestSystemdParseLeases(CiTestCase): +class TestSystemdParseLeases: lxd_lease = dedent( """\ # This is private data. Do not parse. @@ -775,49 +757,40 @@ class TestSystemdParseLeases(CiTestCase): "OPTION_245": "624c3620", } - def setUp(self): - super(TestSystemdParseLeases, self).setUp() - self.lease_d = self.tmp_dir() - - def test_no_leases_returns_empty_dict(self): + def test_no_leases_returns_empty_dict(self, tmp_path): """A leases dir with no lease files should return empty dictionary.""" - self.assertEqual({}, networkd_load_leases(self.lease_d)) + assert {} == networkd_load_leases(str(tmp_path)) - def test_no_leases_dir_returns_empty_dict(self): + def test_no_leases_dir_returns_empty_dict(self, tmp_path): """A non-existing leases dir should return empty dict.""" - enodir = os.path.join(self.lease_d, "does-not-exist") - self.assertEqual({}, networkd_load_leases(enodir)) + enodir = os.path.join(tmp_path, "does-not-exist") + assert {} == networkd_load_leases(enodir) - def test_single_leases_file(self): + def test_single_leases_file(self, tmp_path): """A leases dir with one leases file.""" - populate_dir(self.lease_d, {"2": self.lxd_lease}) - self.assertEqual( - {"2": self.lxd_parsed}, networkd_load_leases(self.lease_d) - ) + populate_dir(str(tmp_path), {"2": self.lxd_lease}) + assert {"2": self.lxd_parsed} == networkd_load_leases(str(tmp_path)) - def test_single_azure_leases_file(self): + def test_single_azure_leases_file(self, tmp_path): """On Azure, option 245 should be present, verify it specifically.""" - populate_dir(self.lease_d, {"1": self.azure_lease}) - self.assertEqual( - {"1": self.azure_parsed}, networkd_load_leases(self.lease_d) - ) + populate_dir(str(tmp_path), {"1": self.azure_lease}) + assert {"1": self.azure_parsed} == networkd_load_leases(str(tmp_path)) - def test_multiple_files(self): + def test_multiple_files(self, tmp_path): """Multiple leases files on azure with one found return that value.""" - self.maxDiff = None populate_dir( - self.lease_d, {"1": self.azure_lease, "9": self.lxd_lease} - ) - self.assertEqual( - {"1": self.azure_parsed, "9": self.lxd_parsed}, - networkd_load_leases(self.lease_d), + str(tmp_path), {"1": self.azure_lease, "9": self.lxd_lease} ) + assert { + "1": self.azure_parsed, + "9": self.lxd_parsed, + } == networkd_load_leases(str(tmp_path)) @responses.activate @pytest.mark.usefixtures("disable_netdev_info") @mock.patch("cloudinit.net.ephemeral._check_connectivity_to_imds") -class TestEphemeralDhcpNoNetworkSetup(CiTestCase): +class TestEphemeralDhcpNoNetworkSetup: @mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") def test_ephemeral_dhcp_no_network_if_url_connectivity( self, m_dhcp, m_imds @@ -832,7 +805,7 @@ def test_ephemeral_dhcp_no_network_if_url_connectivity( MockDistro(), connectivity_urls_data=[{"url": url}], ) as lease: - self.assertIsNone(lease) + assert lease is None # Ensure that no teardown happens: m_dhcp.assert_not_called() @@ -857,7 +830,7 @@ def test_ephemeral_dhcp_setup_network_if_url_connectivity( MockDistro(), connectivity_urls_data=[{"url": url}], ) as lease: - self.assertEqual(m_dhcp.return_value, lease) + assert m_dhcp.return_value == lease # Ensure that dhcp discovery occurs m_dhcp.assert_called_once() @@ -917,10 +890,7 @@ def test_ctx_mgr_umbrella_error(self, m_dhcp, error_class): assert len(m_dhcp.mock_calls) == 1 -class TestUDHCPCDiscoveryClean(CiTestCase): - with_logs = True - maxDiff = None - +class TestUDHCPCDiscoveryClean: @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") @@ -947,16 +917,13 @@ def test_udhcpc_discovery( "routers": "192.168.2.1", "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", } - self.assertEqual( - { - "fixed-address": "192.168.2.74", - "interface": "eth9", - "routers": "192.168.2.1", - "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", - "subnet-mask": "255.255.255.0", - }, - Udhcpc().dhcp_discovery("eth9", distro=MockDistro()), - ) + assert { + "fixed-address": "192.168.2.74", + "interface": "eth9", + "routers": "192.168.2.1", + "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", + "subnet-mask": "255.255.255.0", + } == Udhcpc().dhcp_discovery("eth9", distro=MockDistro()) # Interface was brought up before dhclient called m_subp.assert_has_calls( [ @@ -1016,16 +983,13 @@ def test_udhcpc_discovery_ib( "routers": "192.168.2.1", "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", } - self.assertEqual( - { - "fixed-address": "192.168.2.74", - "interface": "ib0", - "routers": "192.168.2.1", - "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", - "subnet-mask": "255.255.255.0", - }, - Udhcpc().dhcp_discovery("ib0", distro=MockDistro()), - ) + assert { + "fixed-address": "192.168.2.74", + "interface": "ib0", + "routers": "192.168.2.1", + "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", + "subnet-mask": "255.255.255.0", + } == Udhcpc().dhcp_discovery("ib0", distro=MockDistro()) # Interface was brought up before dhclient called m_subp.assert_has_calls( [ @@ -1057,7 +1021,7 @@ def test_udhcpc_discovery_ib( ) -class TestISCDHClient(CiTestCase): +class TestISCDHClient: @mock.patch( "os.listdir", return_value=( @@ -1072,9 +1036,9 @@ def test_get_newest_lease_file_from_distro_rhel(self, *_): """ Test that an rhel style lease has been found """ - self.assertEqual( - "/var/lib/NetworkManager/dhclient-0-u-u-i-d-enp2s0f0.lease", - IscDhclient.get_newest_lease_file_from_distro(rhel.Distro), + assert ( + "/var/lib/NetworkManager/dhclient-0-u-u-i-d-enp2s0f0.lease" + == IscDhclient.get_newest_lease_file_from_distro(rhel.Distro) ) @mock.patch( @@ -1091,9 +1055,9 @@ def test_get_newest_lease_file_from_distro_amazonlinux(self, *_): """ Test that an amazon style lease has been found """ - self.assertEqual( - "/var/lib/dhcp/dhclient--eth0.leases", - IscDhclient.get_newest_lease_file_from_distro(amazon.Distro), + assert ( + "/var/lib/dhcp/dhclient--eth0.leases" + == IscDhclient.get_newest_lease_file_from_distro(amazon.Distro) ) @mock.patch( @@ -1110,9 +1074,9 @@ def test_get_newest_lease_file_from_distro_freebsd(self, *_): """ Test that an freebsd style lease has been found """ - self.assertEqual( - "/var/db/dhclient.leases.vtynet0", - IscDhclient.get_newest_lease_file_from_distro(freebsd.Distro), + assert ( + "/var/db/dhclient.leases.vtynet0" + == IscDhclient.get_newest_lease_file_from_distro(freebsd.Distro) ) @mock.patch( @@ -1129,9 +1093,9 @@ def test_get_newest_lease_file_from_distro_alpine(self, *_): """ Test that an alpine style lease has been found """ - self.assertEqual( - "/var/lib/dhcp/dhclient.leases", - IscDhclient.get_newest_lease_file_from_distro(alpine.Distro), + assert ( + "/var/lib/dhcp/dhclient.leases" + == IscDhclient.get_newest_lease_file_from_distro(alpine.Distro) ) @mock.patch( @@ -1148,9 +1112,9 @@ def test_get_newest_lease_file_from_distro_debian(self, *_): """ Test that an debian style lease has been found """ - self.assertEqual( - "/var/lib/dhcp/dhclient.eth0.leases", - IscDhclient.get_newest_lease_file_from_distro(debian.Distro), + assert ( + "/var/lib/dhcp/dhclient.eth0.leases" + == IscDhclient.get_newest_lease_file_from_distro(debian.Distro) ) # If argument to listdir is '/var/lib/NetworkManager' @@ -1170,11 +1134,10 @@ def test_fallback_when_nothing_found(self, *_): This tests a situation where Distro provides lease information but the lease wasn't found on that location """ - self.assertEqual( - os.path.join(DHCLIENT_FALLBACK_LEASE_DIR, "!@#$-eth0.lease"), - IscDhclient.get_newest_lease_file_from_distro( - rhel.Distro("", {}, {}) - ), + assert os.path.join( + DHCLIENT_FALLBACK_LEASE_DIR, "!@#$-eth0.lease" + ) == IscDhclient.get_newest_lease_file_from_distro( + rhel.Distro("", {}, {}) ) @mock.patch( @@ -1191,9 +1154,8 @@ def test_get_newest_lease_file_from_distro_notfound(self, *_): Test the case when no leases were found """ # Any Distro would suffice for the absense test, choose Centos then. - self.assertEqual( - None, - IscDhclient.get_newest_lease_file_from_distro(centos.Distro), + assert None is IscDhclient.get_newest_lease_file_from_distro( + centos.Distro ) From 442d4518642eb437aed1fdf344053044710f26e8 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 20 Aug 2025 17:29:20 +0200 Subject: [PATCH 087/150] test: drop citestcase from net/test_network_state.py (#6402) --- tests/unittests/net/test_network_state.py | 48 +++++++++++------------ 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/tests/unittests/net/test_network_state.py b/tests/unittests/net/test_network_state.py index e54c04e86ab..0b9a5049967 100644 --- a/tests/unittests/net/test_network_state.py +++ b/tests/unittests/net/test_network_state.py @@ -11,7 +11,6 @@ from cloudinit.net import network_state from cloudinit.net.netplan import Renderer as NetplanRenderer from cloudinit.net.renderers import NAME_TO_RENDERER -from tests.unittests.helpers import CiTestCase netstate_path = "cloudinit.net.network_state" @@ -74,45 +73,45 @@ """ -class TestNetworkStateParseConfig(CiTestCase): - def setUp(self): - super(TestNetworkStateParseConfig, self).setUp() - nsi_path = netstate_path + ".NetworkStateInterpreter" - self.add_patch(nsi_path, "m_nsi") - self.m_nsi: MagicMock +class TestNetworkStateParseConfig: + # pylint: disable=attribute-defined-outside-init + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + self.m_nsi: MagicMock = mocker.patch( + netstate_path + ".NetworkStateInterpreter" + ) def test_missing_version_returns_none(self): ncfg: Dict[str, int] = {} - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): network_state.parse_net_config_data(ncfg) def test_unknown_versions_returns_none(self): ncfg = {"version": 13.2} - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): network_state.parse_net_config_data(ncfg) def test_version_2_passes_self_as_config(self): ncfg = {"version": 2, "otherconfig": {}, "somemore": [1, 2, 3]} network_state.parse_net_config_data(ncfg) - self.assertEqual( - [mock.call(version=2, config=ncfg, renderer=None)], - self.m_nsi.call_args_list, - ) + assert [ + mock.call(version=2, config=ncfg, renderer=None) + ] == self.m_nsi.call_args_list def test_valid_config_gets_network_state(self): ncfg = {"version": 2, "otherconfig": {}, "somemore": [1, 2, 3]} result = network_state.parse_net_config_data(ncfg) - self.assertNotEqual(None, result) + assert None is not result def test_empty_v1_config_gets_network_state(self): ncfg = {"version": 1, "config": []} result = network_state.parse_net_config_data(ncfg) - self.assertNotEqual(None, result) + assert None is not result def test_empty_v2_config_gets_network_state(self): ncfg = {"version": 2} result = network_state.parse_net_config_data(ncfg) - self.assertNotEqual(None, result) + assert None is not result @mock.patch("cloudinit.net.network_state.get_interfaces_by_mac") @@ -320,7 +319,7 @@ def test_v2_nameservers(self, mocker): assert search not in config.dns_searchdomains -class TestNetworkStateHelperFunctions(CiTestCase): +class TestNetworkStateHelperFunctions: def test_mask_to_net_prefix_ipv4(self): netmask_value = "255.255.255.0" expected = 24 @@ -335,9 +334,8 @@ def test_mask_to_net_prefix_all_bits_ipv4(self): def test_mask_to_net_prefix_to_many_bits_ipv4(self): netmask_value = "33" - self.assertRaises( - ValueError, network_state.ipv4_mask_to_net_prefix, netmask_value - ) + with pytest.raises(ValueError): + network_state.ipv4_mask_to_net_prefix(netmask_value) def test_mask_to_net_prefix_all_bits_ipv6(self): netmask_value = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" @@ -353,15 +351,13 @@ def test_mask_to_net_prefix_ipv6(self): def test_mask_to_net_prefix_raises_value_error(self): netmask_value = "ff:ff:ff:ff::" - self.assertRaises( - ValueError, network_state.ipv6_mask_to_net_prefix, netmask_value - ) + with pytest.raises(ValueError): + network_state.ipv6_mask_to_net_prefix(netmask_value) def test_mask_to_net_prefix_to_many_bits_ipv6(self): netmask_value = "129" - self.assertRaises( - ValueError, network_state.ipv6_mask_to_net_prefix, netmask_value - ) + with pytest.raises(ValueError): + network_state.ipv6_mask_to_net_prefix(netmask_value) def test_mask_to_net_prefix_ipv4_object(self): netmask_value = ipaddress.IPv4Address("255.255.255.255") From 3c067c6b747f97d8d3cce92dee3e8443867ebcf0 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Thu, 21 Aug 2025 16:26:30 +0200 Subject: [PATCH 088/150] chore(tox): align check_format's pylint folder targets (#6414) Use the same folder targets of tox -e pylint. Otherwise `tox -e check_format`'s pylint and `tox -e pylint` targets differ, targeting additional folders as doc/ or .tox/ that are not covered by CI. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b0ecb348a69..c8feba0c262 100644 --- a/tox.ini +++ b/tox.ini @@ -97,7 +97,7 @@ deps = {[pinned_versions]deps} commands = {envpython} -m ruff check {posargs:.} - {envpython} -m pylint {posargs:.} + {envpython} -m pylint {posargs:cloudinit/ tests/ tools/} {envpython} -m black --check {posargs:.} {envpython} -m isort --check-only --diff {posargs:.} {envpython} -m mypy {posargs:cloudinit/ tests/ tools/} From 4349e29c003c4e9b77b2dce7aeb0694c8758832a Mon Sep 17 00:00:00 2001 From: abdulganiyy Date: Thu, 21 Aug 2025 16:26:57 +0100 Subject: [PATCH 089/150] fix: fixed defs types in cloud-init analyze show (#6353) --- cloudinit/analyze/__init__.py | 2 +- cloudinit/analyze/show.py | 100 ++++++++++++++++++++++------------ pyproject.toml | 1 - 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/cloudinit/analyze/__init__.py b/cloudinit/analyze/__init__.py index 60ff8daab16..ea2144cb387 100644 --- a/cloudinit/analyze/__init__.py +++ b/cloudinit/analyze/__init__.py @@ -115,7 +115,7 @@ def get_parser( return parser -def analyze_boot(name: str, args: argparse.Namespace) -> int: +def analyze_boot(name: str, args: argparse.Namespace) -> str: """Report a list of how long different boot operations took. For Example: diff --git a/cloudinit/analyze/show.py b/cloudinit/analyze/show.py index b4491bca6cd..56e317bfcfe 100644 --- a/cloudinit/analyze/show.py +++ b/cloudinit/analyze/show.py @@ -8,6 +8,7 @@ import json import sys import time +from typing import IO, Any, Dict, List, Optional, Tuple, Union, cast from cloudinit import subp, util from cloudinit.distros import uses_systemd @@ -50,8 +51,10 @@ CONTAINER_CODE = "container" TIMESTAMP_UNKNOWN = (FAIL_CODE, -1, -1, -1) +Event = Dict -def format_record(msg, event): + +def format_record(msg: str, event: Event) -> str: for i, j in format_key.items(): if i in msg: # ensure consistent formatting of time values @@ -62,56 +65,68 @@ def format_record(msg, event): return msg.format(**event) -def event_name(event): +def event_name(event: Optional[Event]) -> Optional[str]: if event: return event.get("name") return None -def event_type(event): +def event_type(event: Optional[Event]) -> Optional[str]: if event: return event.get("event_type") return None -def event_parent(event): - if event: - return event_name(event).split("/")[0] +def event_parent(event: Optional[Event]) -> Optional[str]: + name = event_name(event) + if name: + return name.split("/")[0] return None -def event_timestamp(event): - return float(event.get("timestamp")) +def event_timestamp(event: Event) -> float: + ts = event.get("timestamp") + if ts is None: + raise ValueError("Event is missing a 'timestamp'") + return float(ts) -def event_datetime(event): +def event_datetime(event: Event) -> datetime.datetime: return datetime.datetime.fromtimestamp( event_timestamp(event), datetime.timezone.utc ) -def delta_seconds(t1, t2): +def delta_seconds(t1: datetime.datetime, t2: datetime.datetime) -> float: return (t2 - t1).total_seconds() -def event_duration(start, finish): +def event_duration(start: Event, finish: Event) -> float: return delta_seconds(event_datetime(start), event_datetime(finish)) -def event_record(start_time, start, finish): +def event_record( + start_time: datetime.datetime, + start: Event, + finish: Event, +) -> Event: record = finish.copy() + name = event_name(start) + indent = "|" + if name: + indent += " " * (name.count("/") - 1) + "`->" record.update( { "delta": event_duration(start, finish), "elapsed": delta_seconds(start_time, event_datetime(start)), - "indent": "|" + " " * (event_name(start).count("/") - 1) + "`->", + "indent": indent, } ) return record -def total_time_record(total_time): +def total_time_record(total_time: float) -> str: return "Total Time: %3.5f seconds\n" % total_time @@ -120,9 +135,10 @@ class SystemctlReader: Class for dealing with all systemctl subp calls in a consistent manner. """ - def __init__(self, property, parameter=None): - self.stdout = None - self.args = [subp.which("systemctl"), "show"] + def __init__(self, property: str, parameter: Optional[str] = None): + self.stdout: Optional[str] = None + self.args: Any = [subp.which("systemctl"), "show"] + if parameter: self.args.append(parameter) # --timestamp=utc is needed for native date strings. Othwerise, @@ -135,9 +151,9 @@ def __init__(self, property, parameter=None): # Don't want the init of our object to break. Instead of throwing # an exception, set an error code that gets checked when data is # requested from the object - self.failure = self.subp() + self.failure = self._subp() - def subp(self): + def _subp(self) -> Optional[Union[str, Exception]]: """ Make a subp call based on set args and handle errors by setting failure code @@ -153,7 +169,7 @@ def subp(self): except Exception as systemctl_fail: return systemctl_fail - def convert_val_to_float(self): + def convert_val_to_float(self) -> float: """ If subp call succeeded, return the timestamp from subp as a float. @@ -167,7 +183,15 @@ def convert_val_to_float(self): "Subprocess call to systemctl has failed, " "returning error code ({})".format(self.failure) ) + # this should never happen as the call to subp succeeded + + if self.stdout is None: + raise RuntimeError( + "stdout of subprocess call to systemctl is None" + ) + # Output from systemctl show has the format Property=Value. + val = self.stdout.split("=")[1].strip() if val.isnumeric(): @@ -191,7 +215,7 @@ def convert_val_to_float(self): return timestamp -def dist_check_timestamp(): +def dist_check_timestamp() -> Tuple[str, float, float, float]: """ Determine which init system a particular linux distro is using. Each init system (systemd, etc) has a different way of @@ -213,7 +237,7 @@ def dist_check_timestamp(): return TIMESTAMP_UNKNOWN -def gather_timestamps_using_dmesg(): +def gather_timestamps_using_dmesg() -> Tuple[str, float, float, float]: """ Gather timestamps that corresponds to kernel begin initialization, kernel finish initialization using dmesg as opposed to systemctl @@ -244,7 +268,7 @@ def gather_timestamps_using_dmesg(): return TIMESTAMP_UNKNOWN -def gather_timestamps_using_systemd(): +def gather_timestamps_using_systemd() -> Tuple[str, float, float, float]: """ Gather timestamps that corresponds to kernel begin initialization, kernel finish initialization. and cloud-init systemd unit activation @@ -302,9 +326,9 @@ def gather_timestamps_using_systemd(): def generate_records( - events, - print_format="(%n) %d seconds in %I%D", -): + events: List[Event], + print_format: str = "(%n) %d seconds in %I%D", +) -> List[List[str]]: """ Take in raw events and create parent-child dependencies between events in order to order events in chronological order. @@ -317,7 +341,7 @@ def generate_records( """ sorted_events = sorted(events, key=lambda x: x["timestamp"]) - records = [] + records: List[str] = [] start_time = None total_time = 0.0 stage_start_time = {} @@ -346,6 +370,9 @@ def generate_records( # see if we have a pair if event_name(event) == event_name(next_evt): if event_type(next_evt) == "finish": + # next_evt is a dictionary because event_type has extracted + # an event type from it: + next_evt = cast(Dict, next_evt) records.append( format_record( print_format, @@ -360,12 +387,15 @@ def generate_records( else: prev_evt = unprocessed.pop() if event_name(event) == event_name(prev_evt): - record = event_record(start_time, prev_evt, event) - records.append( - format_record("Finished stage: (%n) %d seconds", record) - + "\n" - ) - total_time += record.get("delta") + if start_time: + record = event_record(start_time, prev_evt, event) + records.append( + format_record( + "Finished stage: (%n) %d seconds", record + ) + + "\n" + ) + total_time += record.get("delta") or 0.0 else: # not a match, put it back unprocessed.append(prev_evt) @@ -375,7 +405,7 @@ def generate_records( return boot_records -def show_events(events, print_format): +def show_events(events: List[Event], print_format: str) -> List[List[str]]: """ A passthrough method that makes it easier to call generate_records() @@ -388,7 +418,7 @@ def show_events(events, print_format): return generate_records(events, print_format=print_format) -def load_events_infile(infile): +def load_events_infile(infile: IO) -> Tuple[Optional[Any], str]: """ Takes in a log file, read it, and convert to json. diff --git a/pyproject.toml b/pyproject.toml index 3bde817967d..e4ba441d7f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ no_implicit_optional = true # See GH-5445 [[tool.mypy.overrides]] module = [ - "cloudinit.analyze.show", "cloudinit.cmd.devel.make_mime", "cloudinit.cmd.devel.net_convert", "cloudinit.cmd.main", From ffba354cc1100af6f7ba94911608d37fecf2a8d0 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 22 Aug 2025 11:46:51 -0500 Subject: [PATCH 090/150] chore: replace all IRC references with matrix --- CONTRIBUTING.md | 4 ++-- README.md | 2 +- doc/rtd/development/code_review.rst | 2 +- doc/rtd/development/contribute_docs.rst | 6 +++--- doc/rtd/development/find_issues.rst | 2 +- doc/rtd/development/first_PR.rst | 3 +-- doc/rtd/development/index.rst | 4 ++-- doc/rtd/howto/ubuntu_test_prerelease.rst | 5 +++-- doc/rtd/index.rst | 2 +- doc/rtd/links.txt | 2 +- doc/rtd/reference/faq.rst | 6 ++++-- doc/rtd/reference/ubuntu_stable_release_updates.rst | 1 - 12 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4cb2de69bb..c27c862530b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,9 +30,9 @@ Before you can begin, you will need to: ## Getting help -We use IRC and have a dedicated `#cloud-init` channel where you can contact +We use Matrix and have a dedicated `#cloud-init` channel where you can contact us for help and guidance. This link will take you directly to our -[IRC channel on Libera](https://kiwiirc.com/nextclient/irc.libera.chat/cloud-init). +[Matrix room](https://matrix.to/#/#cloud-init:ubuntu.com). Please don't be afraid to reach out if you need help constructing your pull request. diff --git a/README.md b/README.md index fa34fd64ddb..cb50457c147 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ If you need support, start with the [user documentation](https://docs.cloud-init If you need additional help consider reaching out with one of the following options: -- Ask a question in the [``#cloud-init`` IRC channel on Libera](https://kiwiirc.com/nextclient/irc.libera.chat/cloud-init) +- Ask a question in the [``#cloud-init`` channel on Matrix](https://matrix.to/#/#cloud-init:ubuntu.com) - Search the cloud-init [mailing list archive](https://lists.launchpad.net/cloud-init/) * Follow announcements or ask a question on [the cloud-init Discourse forum](https://discourse.ubuntu.com/c/server/cloud-init/) - Join the [cloud-init mailing list](https://launchpad.net/~cloud-init) and participate diff --git a/doc/rtd/development/code_review.rst b/doc/rtd/development/code_review.rst index df8d692b88f..826edc3804d 100644 --- a/doc/rtd/development/code_review.rst +++ b/doc/rtd/development/code_review.rst @@ -26,7 +26,7 @@ help if they need it. If you have questions about the code review process, or need advice on an open PR, these are the available avenues: * Open a PR, add "WIP:" to the title, and leave a comment on that PR -* join the ``#cloud-init`` `channel on the Libera IRC `_ network +* join the ``#cloud-init`` `room on Matrix `_ * post on the ``#cloud-init`` `Discourse topic `_ * send an email to the cloud-init mailing list: :: diff --git a/doc/rtd/development/contribute_docs.rst b/doc/rtd/development/contribute_docs.rst index 810da3a94fd..944b65d3461 100644 --- a/doc/rtd/development/contribute_docs.rst +++ b/doc/rtd/development/contribute_docs.rst @@ -53,9 +53,9 @@ In your first PR ================= If you need some help with your contribution, you can contact us on our -`IRC channel `_. If you have already submitted a work-in-progress PR, you -can also ask for guidance from our technical author by `tagging s-makin`_ as a -reviewer. +`Matrix room `_. If you have already submitted a +work-in-progress PR, you can also ask for guidance from our technical author +by `tagging s-makin`_ as a reviewer. .. LINKS .. include:: ../links.txt diff --git a/doc/rtd/development/find_issues.rst b/doc/rtd/development/find_issues.rst index 8572ece36c3..45240f833cd 100644 --- a/doc/rtd/development/find_issues.rst +++ b/doc/rtd/development/find_issues.rst @@ -53,7 +53,7 @@ improvements you think can be made. This might be related to: * the CSS theming * or even just highlighting things you found confusing or unclear -Feel free to `contact us on IRC `_ if you have other ideas about +Feel free to `contact us on Matrix `_ if you have other ideas about contributions you might want to make, such as blog posts, guides, or tutorials. .. LINKS diff --git a/doc/rtd/development/first_PR.rst b/doc/rtd/development/first_PR.rst index 0aa7bf5cdfb..0512a3bb31e 100644 --- a/doc/rtd/development/first_PR.rst +++ b/doc/rtd/development/first_PR.rst @@ -19,8 +19,7 @@ every pull request to ensure that the CLA has been signed. For any questions or help with the process, email `James Falcon `_ with the subject: -"Cloud-init CLA". You can also contact user ``falcojr`` in the #cloud-init -channel on the `Libera IRC network `_. +"Cloud-init CLA". Create a sandbox environment ============================ diff --git a/doc/rtd/development/index.rst b/doc/rtd/development/index.rst index 18dab526280..df66735be75 100644 --- a/doc/rtd/development/index.rst +++ b/doc/rtd/development/index.rst @@ -35,9 +35,9 @@ Before you can begin, you will need to: Getting help ============ -We use IRC and have a dedicated `#cloud-init` channel where you can contact +We use Matrix and have a dedicated `#cloud-init` room where you can contact us for help and guidance. This link will take you directly to our -`IRC channel on Libera `_. +`room on Matrix `_. Getting started =============== diff --git a/doc/rtd/howto/ubuntu_test_prerelease.rst b/doc/rtd/howto/ubuntu_test_prerelease.rst index cc685a843a1..dab0cde6f15 100644 --- a/doc/rtd/howto/ubuntu_test_prerelease.rst +++ b/doc/rtd/howto/ubuntu_test_prerelease.rst @@ -37,7 +37,7 @@ cloud-init developers missed during testing before cloud-init gets released more broadly. If issues are found during testing, please file a `new cloud-init bug`_ and -leave a message in the `#cloud-init IRC channel`_. +leave a message in the `Matrix room `_. Remove the proposed repository ------------------------------ @@ -58,5 +58,6 @@ This will cause cloud-init to rerun as if it is a first boot. sudo cloud-init clean --logs --reboot +.. LINKS +.. include:: ../links.txt .. _new cloud-init bug: https://github.com/canonical/cloud-init/issues -.. _#cloud-init IRC channel: https://kiwiirc.com/nextclient/irc.libera.chat/cloud-init diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst index 76af68cfcad..dc3e1c19b3a 100644 --- a/doc/rtd/index.rst +++ b/doc/rtd/index.rst @@ -75,7 +75,7 @@ Project and community projects, contributions, suggestions, fixes and constructive feedback. * Read our `Code of Conduct`_ -* Ask questions in the ``#cloud-init`` `IRC channel on Libera `_ +* Ask questions in the ``#cloud-init`` `room on Matrix `_ * Follow announcements or ask a question on `the cloud-init Discourse forum`_ * Join the `cloud-init mailing list`_ * :ref:`Contribute on GitHub` diff --git a/doc/rtd/links.txt b/doc/rtd/links.txt index 05d25107d87..10785c049f7 100644 --- a/doc/rtd/links.txt +++ b/doc/rtd/links.txt @@ -5,8 +5,8 @@ .. _Diataxis: https://diataxis.fr/ .. _GH repo: https://github.com/canonical/cloud-init .. _GitHub: https://github.com -.. _IRC: https://kiwiirc.com/nextclient/irc.libera.chat/cloud-init .. _Launchpad: https://launchpad.net +.. _Matrix: https://matrix.to/#/#cloud-init:ubuntu.com .. _the docs: https://docs.cloud-init.io/en/latest/ .. _tox: https://tox.readthedocs.io/en/latest/ .. _Discourse: https://discourse.ubuntu.com/c/server/cloud-init/54 diff --git a/doc/rtd/reference/faq.rst b/doc/rtd/reference/faq.rst index 146dc66774d..6dbeeefa62d 100644 --- a/doc/rtd/reference/faq.rst +++ b/doc/rtd/reference/faq.rst @@ -10,7 +10,7 @@ Having trouble? We would like to help! - First go through this page with answers to common questions - Use the search bar at the upper left to search our documentation -- Ask questions in the ``#cloud-init`` `IRC channel on Libera`_ +- Ask questions in the ``#cloud-init`` `Matrix room `_ - Join and ask questions on the ``cloud-init`` `mailing list`_ - Find a bug? Check out the :ref:`reporting_bugs` topic to find out how to report one @@ -76,7 +76,6 @@ Whitepapers: - `Cloud Instance Initialization with cloud-init (Whitepaper)`_ .. _mailing list: https://launchpad.net/~cloud-init -.. _IRC channel on Libera: https://kiwiirc.com/nextclient/irc.libera.chat/cloud-init .. _do: https://github.com/canonical/ubuntu-pro-client/blob/9b46480b9e4b88e918bac5ced0d4b8edb3cbbeab/lib/auto_attach.py#L35 .. _cloud-init - The Good Parts: https://www.youtube.com/watch?v=2_m6EUo6VOI @@ -98,3 +97,6 @@ Whitepapers: .. _cloud-init Summit 2018: https://powersj.io/post/cloud-init-summit18/ .. _cloud-init Summit 2017: https://powersj.io/post/cloud-init-summit17/ .. _Subiquity autoinstaller: https://ubuntu.com/server/docs/install/autoinstall + +.. LINKS +.. include:: ../links.txt diff --git a/doc/rtd/reference/ubuntu_stable_release_updates.rst b/doc/rtd/reference/ubuntu_stable_release_updates.rst index 1babc25789d..ffb7d77cdf1 100644 --- a/doc/rtd/reference/ubuntu_stable_release_updates.rst +++ b/doc/rtd/reference/ubuntu_stable_release_updates.rst @@ -47,6 +47,5 @@ The `integration test suite` used for validation follows these steps: .. _SRU: https://wiki.ubuntu.com/StableReleaseUpdates .. _CloudinitUpdates: https://wiki.ubuntu.com/CloudinitUpdates .. _new cloud-init bug: https://github.com/canonical/cloud-init/issues -.. _#cloud-init IRC channel: https://kiwiirc.com/nextclient/irc.libera.chat/cloud-init .. _integration test suite: https://github.com/canonical/cloud-init/tree/main/tests/integration_tests .. _SRU release version: https://github.com/canonical/ubuntu-maintainers-handbook/blob/main/VersionStrings.md#version-adding-a-change-in-ubuntu-as-a-stable-release-update From 9648ad628d996f3e4261034029ca44a3a7e05671 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 22 Aug 2025 12:03:36 -0500 Subject: [PATCH 091/150] chore: replace all mailing-list references with GH Discussions --- README.md | 4 +--- SECURITY.md | 4 ++-- doc/rtd/development/code_review.rst | 7 ++----- doc/rtd/index.rst | 8 ++------ doc/rtd/links.txt | 1 + doc/rtd/reference/faq.rst | 3 +-- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cb50457c147..67c98a35714 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,7 @@ If you need support, start with the [user documentation](https://docs.cloud-init If you need additional help consider reaching out with one of the following options: - Ask a question in the [``#cloud-init`` channel on Matrix](https://matrix.to/#/#cloud-init:ubuntu.com) -- Search the cloud-init [mailing list archive](https://lists.launchpad.net/cloud-init/) -* Follow announcements or ask a question on [the cloud-init Discourse forum](https://discourse.ubuntu.com/c/server/cloud-init/) -- Join the [cloud-init mailing list](https://launchpad.net/~cloud-init) and participate +- Follow announcements or ask a question on [GitHub Discussions](https://github.com/canonical/cloud-init/discussions) - Find a bug? [Report bugs on GitHub Issues](https://github.com/canonical/cloud-init/issues) ## Distribution and cloud support diff --git a/SECURITY.md b/SECURITY.md index ebb8a7e5437..7c4df9b27c4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ The following documents the upstream cloud-init security policy. ## Reporting If a security bug is found, please send an email to -cloud-init-security@lists.canonical.com . After the bug is received, + . After the bug is received, the issue is triaged within 2 working days of being reported and a response is sent to the reporter. @@ -44,7 +44,7 @@ determined time for disclosure has arrived the following will occur: * A public bug is filed/made public with vulnerability details, CVE, mitigations and where to obtain the fix -* An email is sent to the [public cloud-init mailing list](https://lists.launchpad.net/cloud-init/) +* An announcement is made to [GitHub Discussions](https://github.com/canonical/cloud-init/discussions) The disclosure timeframe is coordinated with the reporter and members of the cloud-init-security list. This depends on a number of factors: diff --git a/doc/rtd/development/code_review.rst b/doc/rtd/development/code_review.rst index 826edc3804d..bde3f2328b2 100644 --- a/doc/rtd/development/code_review.rst +++ b/doc/rtd/development/code_review.rst @@ -26,11 +26,8 @@ help if they need it. If you have questions about the code review process, or need advice on an open PR, these are the available avenues: * Open a PR, add "WIP:" to the title, and leave a comment on that PR -* join the ``#cloud-init`` `room on Matrix `_ -* post on the ``#cloud-init`` `Discourse topic `_ -* send an email to the cloud-init mailing list: :: - - cloud-init@lists.launchpad.net +* join the ``#cloud-init`` `channel on Matrix `_ +* post on `Github Discussions`_ These are listed in order of our preference, but please use whichever of them you are most comfortable with. diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst index dc3e1c19b3a..6f846679f3e 100644 --- a/doc/rtd/index.rst +++ b/doc/rtd/index.rst @@ -65,7 +65,7 @@ Having trouble? We would like to help! - :ref:`Check out our tutorials` if you're new to ``cloud-init`` - :ref:`Try the FAQ` for answers to some common questions -- You can also search the ``cloud-init`` `mailing list archive`_ +- You can also check `Github Discussions`_ - Find a bug? `Report bugs on GitHub Issues`_ Project and community @@ -76,8 +76,7 @@ projects, contributions, suggestions, fixes and constructive feedback. * Read our `Code of Conduct`_ * Ask questions in the ``#cloud-init`` `room on Matrix `_ -* Follow announcements or ask a question on `the cloud-init Discourse forum`_ -* Join the `cloud-init mailing list`_ +* Follow announcements or ask a question on `GitHub Discussions`_ * :ref:`Contribute on GitHub` * `Release schedule`_ @@ -104,8 +103,5 @@ projects, contributions, suggestions, fixes and constructive feedback. .. LINKS .. include:: links.txt -.. _the cloud-init Discourse forum: https://discourse.ubuntu.com/c/project/cloud-init/54 -.. _cloud-init mailing list: https://launchpad.net/~cloud-init -.. _mailing list archive: https://lists.launchpad.net/cloud-init/ .. _Release schedule: https://discourse.ubuntu.com/t/2025-cloud-init-release-schedule/55534 .. _Report bugs on GitHub Issues: https://github.com/canonical/cloud-init/issues diff --git a/doc/rtd/links.txt b/doc/rtd/links.txt index 10785c049f7..0381ab31fca 100644 --- a/doc/rtd/links.txt +++ b/doc/rtd/links.txt @@ -3,6 +3,7 @@ .. _CLA: https://ubuntu.com/legal/contributors .. _Code of Conduct: https://ubuntu.com/community/code-of-conduct .. _Diataxis: https://diataxis.fr/ +.. _GitHub Discussions: https://github.com/canonical/cloud-init/discussions .. _GH repo: https://github.com/canonical/cloud-init .. _GitHub: https://github.com .. _Launchpad: https://launchpad.net diff --git a/doc/rtd/reference/faq.rst b/doc/rtd/reference/faq.rst index 6dbeeefa62d..225ea642ffa 100644 --- a/doc/rtd/reference/faq.rst +++ b/doc/rtd/reference/faq.rst @@ -11,7 +11,7 @@ Having trouble? We would like to help! - First go through this page with answers to common questions - Use the search bar at the upper left to search our documentation - Ask questions in the ``#cloud-init`` `Matrix room `_ -- Join and ask questions on the ``cloud-init`` `mailing list`_ +- Join and ask questions on `Github Discussions`_ - Find a bug? Check out the :ref:`reporting_bugs` topic to find out how to report one @@ -75,7 +75,6 @@ Whitepapers: - `Utilising cloud-init on Microsoft Azure (Whitepaper)`_ - `Cloud Instance Initialization with cloud-init (Whitepaper)`_ -.. _mailing list: https://launchpad.net/~cloud-init .. _do: https://github.com/canonical/ubuntu-pro-client/blob/9b46480b9e4b88e918bac5ced0d4b8edb3cbbeab/lib/auto_attach.py#L35 .. _cloud-init - The Good Parts: https://www.youtube.com/watch?v=2_m6EUo6VOI From e0bf72661619cc9a5138d4695f10603d179f85c7 Mon Sep 17 00:00:00 2001 From: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:40:30 +0530 Subject: [PATCH 092/150] chore: render vlan, bond, bridge mac properly in network state (#6397) Signed-off-by: Shreenidhi Shedi --- cloudinit/net/network_state.py | 8 +++++++ cloudinit/net/networkd.py | 38 +++------------------------------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index ab7bb2c9a50..be87a8f5f17 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -432,6 +432,10 @@ def handle_physical(self, command): "keep_configuration": command.get("keep_configuration"), } ) + + if iface["mac_address"]: + iface["mac_address"] = iface["mac_address"].lower() + iface_key = command.get("config_id", command.get("name")) self._network_state["interfaces"].update({iface_key: iface}) self.dump_network_state() @@ -791,6 +795,7 @@ def handle_vlans(self, command): "name": vlan, "vlan_id": cfg.get("id"), "vlan_link": cfg.get("link"), + "mac_address": cfg.get("macaddress"), } if "mtu" in cfg: vlan_cmd["mtu"] = cfg["mtu"] @@ -848,8 +853,11 @@ def _handle_bond_bridge(self, command, cmd_type=None): cmd_type + "_interfaces": item_cfg.get("interfaces"), "params": dict((v2key_to_v1[k], v) for k, v in params.items()), } + if "mtu" in item_cfg: v1_cmd["mtu"] = item_cfg["mtu"] + if "macaddress" in item_cfg: + v1_cmd["mac_address"] = item_cfg["macaddress"] warn_deprecated_all_devices(item_cfg) subnets = self._v2_to_v1_ipcfg(item_cfg) diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py index ae1e83c2c6b..efeedfd29e4 100644 --- a/cloudinit/net/networkd.py +++ b/cloudinit/net/networkd.py @@ -396,7 +396,6 @@ def _render_content(self, ns: NetworkState): vlan_dict = self.render_vlans(ns) vlan_netdev = vlan_dict["vlan_netdev"] vlan_link = vlan_dict["vlan_link"] - vlan_link["macaddress"] = vlan_dict["vlan_mac_info"] ret_dict["vlan_netdev"] = vlan_netdev if "bonds" in ns.config: @@ -426,24 +425,10 @@ def _render_content(self, ns: NetworkState): if rPolicy: self.parseRoutingPolicy(rPolicy, cfg) - # TODO: revisit this once network state renders macaddress - # properly for vlan config - if not iface["mac_address"] and vlan_link.get("macaddress"): - mac = vlan_link["macaddress"].get(iface_name) - if mac: - iface["mac_address"] = mac - bond_link_name = bond_link.get(iface_name) if bond_link_name: cfg.update_section("Network", "Bond", bond_link_name) - # TODO: revisit this once network state renders macaddress - # properly for bond config - if not iface["mac_address"] and bond_link.get("macaddress"): - mac = bond_link["macaddress"].get(iface_name) - if mac: - iface["mac_address"] = mac - bridge_link_name = bridge_link.get(iface_name) if bridge_link_name: val = ( @@ -464,13 +449,6 @@ def _render_content(self, ns: NetworkState): cfg.update_section("Network", "Bridge", bridge_link_name) - # TODO: revisit this once network state renders macaddress - # properly for bridge config - if not iface["mac_address"] and bridge_link.get("macaddress"): - mac = bridge_link["macaddress"].get(iface_name) - if mac: - iface["mac_address"] = mac - link = self.generate_match_section(iface, cfg) self.generate_link_section(iface, cfg) @@ -527,7 +505,6 @@ def _render_content(self, ns: NetworkState): def render_vlans(self, ns: NetworkState) -> dict: vlan_link_info = defaultdict(list) - vlan_mac_info = {} vlan_ndev_configs = {} vlans = ns.config.get("vlans", {}) @@ -554,9 +531,7 @@ def render_vlans(self, ns: NetworkState) -> dict: val = vlan_cfg.get("macaddress") if val: - val = val.lower() - cfg.update_section("NetDev", "MACAddress", val) - vlan_mac_info[vlan_name] = val + cfg.update_section("NetDev", "MACAddress", val.lower()) cfg.update_section("VLAN", "Id", vlan_id) vlan_ndev_configs[vlan_name] = cfg.get_final_conf() @@ -564,7 +539,6 @@ def render_vlans(self, ns: NetworkState) -> dict: return { "vlan_netdev": vlan_ndev_configs, "vlan_link": vlan_link_info, - "vlan_mac_info": vlan_mac_info, } def render_bonds(self, ns: NetworkState) -> dict: @@ -572,8 +546,6 @@ def render_bonds(self, ns: NetworkState) -> dict: bond_ndev_configs = {} section = "Bond" - bond_link_info["macaddress"] = {} - bonds = ns.config.get("bonds", {}) for bond_name, bond_cfg in bonds.items(): interfaces = bond_cfg.get("interfaces") @@ -596,9 +568,7 @@ def render_bonds(self, ns: NetworkState) -> dict: val = bond_cfg.get("macaddress") if val: - val = val.lower() - cfg.update_section("NetDev", "MACAddress", val) - bond_link_info["macaddress"][bond_name] = val + cfg.update_section("NetDev", "MACAddress", val.lower()) # Optional bond parameters params = bond_cfg.get("parameters", {}) @@ -678,9 +648,7 @@ def render_bridges(self, ns: NetworkState) -> dict: val = bridge_cfg.get("macaddress") if val: - val = val.lower() - cfg.update_section("NetDev", "MACAddress", val) - bridge_link_info["macaddress"][bridge_name] = val + cfg.update_section("NetDev", "MACAddress", val.lower()) # Bridge parameters params = bridge_cfg.get("parameters", {}) From cf59dba848404477af7149c88f2bc109a56ab99b Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 22 May 2025 09:35:41 -0600 Subject: [PATCH 093/150] chore!: switch to meson.build due to PEP632 (#6326) setuptools deprecated use of data_files for non-package external data files or system files for some time[1],[2] as part of PEP632[3] work. This PEP632 incompatiblity prompts some distributions to drop support for cloud-init in their operating system[4]. Rather than pursue the use of setup.py via setuptools further, shift to meson which better supports downstream operating system-level packaging of ancillary distribution of man pages, config files, udev, openrc or systemd scripts which are intended to live outside of a pure python distribution. Since cloudinit package is primarily intended to be delivered as system library and utility for initial system setup during first boot, it is out of the scope of cloud-init to support local wheel or bdist installations via pypi in custom locations outside of system packages as we want only one system-package installation of cloud-init in any environment. As such, cloud-init's use of python setuptools is not necessary. A move to meson gives a bit more flexibility to handle parameterized delivery of the supplemental config, init scripts, man pages an tooling which can be extended for cross-platform build support. This commit creates an initial draft of the work to migrate cloud-init backend to use meson >= 0.63.0. - Drop setup.py and replace that functionality with meson.build files which perform the same setup, template rendering and installs previously done by setuptools. - drop build-system backend from pyproject.toml to avoid automatic RTD meson-builds - Add test targets and dependency install targets to meson.build - Set pyproject.toml to use meson build backend - Drop now unused setup_utils and moving pep400 func to test_tools.py - update dev testing tools under packages to provide reference SPEC and debian/* files and allow for local development package builds References: - [1] https://setuptools.pypa.io/en/latest/references/keywords.html - [2] https://setuptools.pypa.io/en/latest/userguide/datafiles.html#non-package-data-files - [3] https://peps.python.org/pep-0632/ Closes: GH-3986 Fixes: GH-5027 LP: #1978328 --- .gitignore | 1 + .readthedocs.yaml | 5 +- doc-requirements.txt | 2 +- doc/rtd/conf.py | 10 +- doc/rtd/development/packaging.rst | 81 ++++++ doc/rtd/index.rst | 1 + doc/rtd/reference/breaking_changes.rst | 13 + meson.build | 206 +++++++++++++++ meson_options.txt | 3 + packages/bddeb | 2 +- packages/brpm | 2 +- packages/debian/rules | 9 +- packages/pkg-deps.json | 12 +- packages/redhat/cloud-init.spec.in | 31 +-- pyproject.toml | 6 +- scripts/cloud-id | 9 + scripts/cloud-init | 9 + setup.py | 342 ------------------------- setup_utils.py | 60 ----- test-requirements.txt | 2 +- tests/unittests/net/test_dhcp.py | 3 +- tools/run-container | 2 +- tools/test_tools.py | 27 +- tox.ini | 2 - 24 files changed, 383 insertions(+), 457 deletions(-) create mode 100644 doc/rtd/development/packaging.rst create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 scripts/cloud-id create mode 100644 scripts/cloud-init delete mode 100644 setup.py delete mode 100644 setup_utils.py diff --git a/.gitignore b/.gitignore index 7d671fb9f21..13032535c79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.asc *.build *.buildinfo +*.builddir *.changes *.cover *.deb diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f5a56263916..edbc723ed28 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,13 +3,14 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.10" + python: "3.11" +sphinx: + configuration: doc/rtd/conf.py formats: all python: install: - - path: . - requirements: doc-requirements.txt sphinx: diff --git a/doc-requirements.txt b/doc-requirements.txt index 2bb66b5d2dc..78a0a22f24a 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,9 +1,9 @@ -r requirements.txt doc8 furo +meson m2r2 pyyaml -setuptools sphinx==7.1.2 sphinx-design sphinx-copybutton diff --git a/doc/rtd/conf.py b/doc/rtd/conf.py index 212ca832383..3558750262f 100644 --- a/doc/rtd/conf.py +++ b/doc/rtd/conf.py @@ -3,10 +3,6 @@ import os import sys -from cloudinit import version -from cloudinit.config.schema import get_schema -from cloudinit.handlers.jinja_template import render_jinja_payload - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -15,6 +11,12 @@ sys.path.insert(0, os.path.abspath("./")) sys.path.insert(0, os.path.abspath(".")) +# Readthedocs builds will pip install packages and not have PYTHONPATH set +# So avoid cloudinit imports until we have updated our path first. +from cloudinit import version +from cloudinit.config.schema import get_schema +from cloudinit.handlers.jinja_template import render_jinja_payload + # Suppress warnings for docs that aren't used yet # unused_docs = [ # ] diff --git a/doc/rtd/development/packaging.rst b/doc/rtd/development/packaging.rst new file mode 100644 index 00000000000..d5ee1b88c7b --- /dev/null +++ b/doc/rtd/development/packaging.rst @@ -0,0 +1,81 @@ +.. _downstream_packaging: + +Downstream packaging +******************** + +This page is intended to support operating system packagers of ``cloud-init`` +and is not intended for other audiences to generate their own custom cloud-init +packages. + +``Cloud-init`` is not published to PyPI as it is not intended to be consumed +as a pure-python package or run from virtual environments or python paths that +are not system-wide. + +Guidelines +========== + +Build Dependencies +------------------ +The following build dependencies must be available on the system: + +- ``python3`` +- ``meson >= 0.63.0`` +- ``pkgconf`` +- ``bash-completion`` + +Additional dependencies for systemd environments: + +- ``systemd-devel`` +- ``udev`` + +The full list of all package build-dependencies for a given +distribution can be obtained by the following command: + +.. code-block:: bash + + ./tools/read-dependencies --system --distro= + + +Manual build procedure +---------------------- + +Meson install directory locations may be set with +``meson setup -D=``. + +Steps to validate ``cloud-init`` package builds in a development environment: + +.. code-block:: bash + + meson setup builddir + meson test -C builddir -v + meson install -C builddir --destdir=testinstall + # List installed files + find builddir/testinstall/ + + +Test builds of RPMs or DEBs +--------------------------- +To ease the development and testing of local changes, development-quality DEB +or RPM packages can be built with one of the following scripts on a build host +which already has all system build dependencies installed: + +.. code-block:: bash + + ./packages/brpm --distro=redhat # or --distro=suse to build an RPM + ./packages/bddeb -d # to build a DEB + +OR if LXC is present, the full package build can be run in a container: + +.. code-block:: bash + + ./tools/run-container ubuntu-daily:plucky --package --keep + ./tools/run-container rockylinux/9 --package --keep + + +.. note:: + + meson support has not yet been added to the BSDs in :file:`tools/build-on-*bsd` or :file:`meson.build`. + + +.. LINKS: +.. _meson: https://mesonbuild.com/ diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst index 6f846679f3e..9d1f1543a54 100644 --- a/doc/rtd/index.rst +++ b/doc/rtd/index.rst @@ -99,6 +99,7 @@ projects, contributions, suggestions, fixes and constructive feedback. Contribute to code Contribute to docs Community + Downstream packaging .. LINKS diff --git a/doc/rtd/reference/breaking_changes.rst b/doc/rtd/reference/breaking_changes.rst index 1f5d613ccd8..434e0e0dd4c 100644 --- a/doc/rtd/reference/breaking_changes.rst +++ b/doc/rtd/reference/breaking_changes.rst @@ -11,6 +11,18 @@ releases. many operating system vendors patch out breaking changes in cloud-init to ensure consistent behavior on their platform. +25.3 +==== + +**For cloud-init downstream packagers**: the packaging build backend changed +to meson and away from python's setuptools.distutils in response to +`PEP-0632's deprecation of distutils`_. Meson packaging may not be identical +to previous binary packages generated by python's setuptools. Downstream +packagers should validate the final generated package to ensure +unexpected package deltas do not exist. + +See :ref:`downstream_packaging` for more info on packaging cloud-init. + 25.1.4 ====== @@ -252,3 +264,4 @@ a ``datasource_list`` in ``/etc/cloud/cloud.cfg.d/*.cfg``. .. _attach a ConfigDrive: https://docs.openstack.org/nova/2024.1/admin/config-drive.html .. _this patch: https://github.com/canonical/cloud-init/blob/ubuntu/noble/debian/patches/no-single-process.patch .. _Python3 equivalent: https://github.com/canonical/cloud-init/pull/5489#issuecomment-2408210561 +.. _PEP-0632's deprecation of distutils: https://peps.python.org/pep-0632/ diff --git a/meson.build b/meson.build new file mode 100644 index 00000000000..5b1851dbe1d --- /dev/null +++ b/meson.build @@ -0,0 +1,206 @@ +project( + 'cloud-init', + meson_version: '>=0.63.0', # rockylinux/9 + license: 'GPL-3 OR Apache-2.0', + default_options: [ + # The default can yield broken results. + 'python.install_env=auto', + ], +) +system = host_machine.system() +sysconfdir = get_option('sysconfdir') +init_system = get_option('init_system') + +lib_exec_dir = get_option('prefix') / get_option('libexecdir') / 'cloud-init' +render_tmpl = './tools/render-template' + +pymod = import('python') +python = pymod.find_installation('python3') + +# https://mesonbuild.com/FAQ.html#but-i-really-want-to-use-wildcards for downsteam drop-in files +find = find_program('find') + +install_subdir( + 'cloudinit', + install_dir: python.get_install_dir(), + install_tag: 'python-runtime', + install_mode: 'rw-r--r--', +) + +# Binaries and script entrypoints +bash_completions_dir = dependency('bash-completion').get_variable( + pkgconfig: 'completionsdir', + default_value: get_option('datadir') / 'bash-completion' / 'completions', +) +install_data( + 'bash_completion/cloud-init', + install_dir: bash_completions_dir, + install_mode: 'rw-r--r--', + install_tag: 'bin', +) + +install_data( + [ + 'tools/ds-identify', + 'tools/hook-hotplug', + 'tools/uncloud-init', + 'tools/write-ssh-key-fingerprints', + ], + install_dir: lib_exec_dir, + install_mode: 'rwxr-xr-x', + install_tag: 'bin', +) + +install_data( + ['tools/cloud-init-per', 'scripts/cloud-id', 'scripts/cloud-init'], + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + install_tag: 'bin', +) + +# Required Config and Templates +cfgs = run_command(find, 'config/cloud.cfg.d', '-type', 'f', '-name', '*.cfg', check: true) +install_data( + ['config/cloud.cfg.d/README'] + cfgs.stdout().strip().split('\n'), + install_dir: sysconfdir / 'cloud' / 'cloud.cfg.d', + install_mode: 'rw-r--r--', + install_tag: 'config', +) + +distro_templates = get_option('distro_templates') +if distro_templates.length() == 0 + templates = run_command(find, 'templates', '-type', 'f', '-name', '*.tmpl', check: true).stdout().strip().split('\n') +else + templates = [] + foreach template : distro_templates + templates += 'templates' / template + endforeach +endif + +install_data( + templates, + install_dir: sysconfdir / 'cloud' / 'templates', + install_mode: 'rw-r--r--', + install_tag: 'config', +) + +if init_system == 'systemd' + systemd = dependency('systemd') + udev = dependency('udev') + systemd_unit_dir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir') + systemd_generator_dir = systemd.get_variable(pkgconfig: 'systemdsystemgeneratordir') + udev_dir = udev.get_variable(pkgconfig: 'udevdir') + install_data( + 'udev/66-azure-ephemeral.rules', + install_dir: udev_dir / 'rules.d', + install_mode: 'rw-r--r--', + install_tag: 'systemd', + ) + + # Must generate systemd templates in root mesonbuild, because nested + # systemd/meson.build results in builddir @OUTPUT@ macro being doubly-nested + # paths + custom_target( + input: 'systemd/cloud-init-generator.tmpl', + output: '@BASENAME@', + command: [ + render_tmpl, + '@INPUT@', + meson.current_build_dir() / '@OUTPUT@', + ], + install: true, + install_dir: systemd_generator_dir, + install_mode: 'rwxr-xr-x', + install_tag: 'systemd', + ) + + systemd_templates = run_command( + find, + 'systemd', + '-not', + '-name', '*generator*', + '-name', '*.tmpl', + check: true, + ) + foreach template : systemd_templates.stdout().strip().split('\n') + custom_target( + input: template, + output: '@BASENAME@', + command: [ + render_tmpl, + '@INPUT@', + meson.current_build_dir() / '@OUTPUT@', + ], + install: true, + install_dir: systemd_unit_dir, + install_mode: 'rw-r--r--', + install_tag: 'systemd', + ) + endforeach + + if get_option('disable_sshd_keygen') + # Typically on Fedora systems and sshd_keygen service can race cloud-init. + install_data( + 'systemd/disable-sshd-keygen-if-cloud-init-active.conf', + install_dir: systemd_unit_dir / 'sshd-keygen@.service.d', + install_mode: 'rw-r--r--', + install_tag: 'systemd', + ) + endif + + # Allow for downstream supplements of services files meson.build changes + install_data( + run_command(find, 'systemd', '-name', '*.service', check: true).stdout().strip().split('\n') + + run_command(find, 'systemd', '-name', '*.target', check: true).stdout().strip().split('\n') + + run_command(find, 'systemd', '-name', '*.socket', check: true).stdout().strip().split('\n'), + install_dir: systemd_unit_dir, + install_mode: 'rw-r--r--', + install_tag: 'systemd', + ) +endif + +custom_target( + input: 'config/cloud.cfg.tmpl', + output: 'cloud.cfg', + command: [ + render_tmpl, + '--is-yaml', '@INPUT@', + meson.current_build_dir() / '@OUTPUT@', + ], + install: true, + install_dir: sysconfdir / 'cloud', + install_mode: 'rw-r--r--', + install_tag: 'config', +) + +# Docs and Examples: +install_man(['doc/man/cloud-init.1', 'doc/man/cloud-id.1', 'doc/man/cloud-init-per.1']) +examples = run_command(find, 'doc/examples', '-name', '*.txt', '-o', '-name', '*.yaml', check: true) +install_data( + examples.stdout().strip().split('\n'), + install_dir: get_option('datadir') / 'doc' / 'cloud-init' / 'examples', + install_mode: 'rw-r--r--', + install_tag: 'doc', +) +install_data( + [ + 'doc/examples/seed/README', + 'doc/examples/seed/meta-data', + 'doc/examples/seed/user-data', + ], + install_dir: get_option('datadir') / 'doc' / 'cloud-init' / 'examples' / 'seed', + install_mode: 'rw-r--r--', + install_tag: 'doc', +) + +# Create expected packaging directory structure +meson.add_install_script('sh', '-c', 'mkdir -p ${DESTDIR}/' + sysconfdir + '/cloud/clean.d') + +# meson setup builddir -Dinit_system=systemd +# meson test -C builddir -v +test( + 'py3', + python, + args: ['-m', 'pytest', meson.project_source_root() / 'tests' / 'unittests'], + timeout: 0, +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000000..f75f408778a --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,3 @@ +option('init_system', type: 'string', value: 'systemd', description: 'Set target init system.') +option('distro_templates', type: 'array', value: [], description: 'Distro template files to install. WARNING: Templates may change in the future. If using this option, be sure to check new releases for template file changes.') +option('disable_sshd_keygen', type: 'boolean', value: false, description: 'Provide systemd service to disable sshd-keygen if present to avoid races with cloud-init.') diff --git a/packages/bddeb b/packages/bddeb index af9fbf89b8b..162a58cb0c0 100755 --- a/packages/bddeb +++ b/packages/bddeb @@ -19,7 +19,7 @@ def find_root(): top_dir = os.path.dirname( os.path.dirname(os.path.abspath(sys.argv[0])) ) - if os.path.isfile(os.path.join(top_dir, "setup.py")): + if os.path.isfile(os.path.join(top_dir, "meson.build")): return os.path.abspath(top_dir) raise OSError( ( diff --git a/packages/brpm b/packages/brpm index e942dbae8be..b942aa89a53 100755 --- a/packages/brpm +++ b/packages/brpm @@ -16,7 +16,7 @@ def find_root(): top_dir = os.path.dirname( os.path.dirname(os.path.abspath(sys.argv[0])) ) - if os.path.isfile(os.path.join(top_dir, "setup.py")): + if os.path.isfile(os.path.join(top_dir, "meson.build")): return os.path.abspath(top_dir) raise OSError( ( diff --git a/packages/debian/rules b/packages/debian/rules index b9b8eaff5fe..bfd2779ba74 100755 --- a/packages/debian/rules +++ b/packages/debian/rules @@ -2,13 +2,14 @@ include /usr/share/dpkg/pkg-info.mk -INIT_SYSTEM ?= systemd -export PYBUILD_INSTALL_ARGS=--init-system=$(INIT_SYSTEM) - %: - dh $@ --with python3 --buildsystem pybuild + dh $@ --buildsystem meson + +override_dh_auto_configure: + dh_auto_configure -- -Dinit_system=systemd -Dlibexecdir=lib override_dh_auto_test: + ifeq (,$(findstring nocheck,$(DEB_BUILD_OPTIONS))) http_proxy= make PYVER=python3 check else diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json index 4ee0982a55e..bb3ecf12ede 100644 --- a/packages/pkg-deps.json +++ b/packages/pkg-deps.json @@ -1,9 +1,11 @@ { "debian" : { "build-requires" : [ + "meson", + "pkgconf", + "bash-completion", "debhelper", - "dh-python", - "python3-debconf" + "systemd-dev" ], "renames" : { "pyyaml" : "python3-yaml", @@ -43,7 +45,11 @@ }, "redhat" : { "build-requires" : [ - "python3-devel" + "bash-completion", + "meson", + "pkgconf", + "python3-devel", + "systemd-devel" ], "requires" : [ "e2fsprogs", diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in index a09e87d290c..c598d722b9f 100644 --- a/packages/redhat/cloud-init.spec.in +++ b/packages/redhat/cloud-init.spec.in @@ -14,11 +14,11 @@ Name: cloud-init Version: {{rpm_upstream_version}} Release: 1{{subrelease}}%{?dist} -Summary: Cloud instance init scripts +Summary: Cloud instance initilization tool Group: System Environment/Base License: Dual-licesed GPLv3 or Apache 2.0 -URL: http://launchpad.net/cloud-init +URL: https://github.com/canonical/cloud-init Source0: {{archive_name}} BuildArch: noarch @@ -86,13 +86,11 @@ ssh keys and to let the user run various scripts. {% endfor %} %build -%{__python3} setup.py build +%meson -Dinit_system=systemd +%meson_build %install - -%{__python3} setup.py install -O1 \ - --skip-build --root $RPM_BUILD_ROOT \ - --init-system=%{init_system} +%meson_install # Note that /etc/rsyslog.d didn't exist by default until F15. # el6 request: https://bugzilla.redhat.com/show_bug.cgi?id=740420 @@ -100,9 +98,6 @@ mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d cp -p tools/21-cloudinit.conf \ $RPM_BUILD_ROOT/%{_sysconfdir}/rsyslog.d/21-cloudinit.conf -# Remove the tests -rm -rf $RPM_BUILD_ROOT%{python3_sitelib}/tests - # Required dirs... mkdir -p $RPM_BUILD_ROOT/%{_sharedstatedir}/cloud mkdir -p $RPM_BUILD_ROOT/%{_libexecdir}/%{name} @@ -122,10 +117,11 @@ rm -rf $RPM_BUILD_ROOT %if "%{init_system}" == "systemd" if [ $1 -eq 1 ] then + /bin/systemctl enable cloud-init-main.service >/dev/null 2>&1 || : + /bin/systemctl enable cloud-init-local.service >/dev/null 2>&1 || : + /bin/systemctl enable cloud-init-network.service >/dev/null 2>&1 || : /bin/systemctl enable cloud-config.service >/dev/null 2>&1 || : /bin/systemctl enable cloud-final.service >/dev/null 2>&1 || : - /bin/systemctl enable cloud-init-network.service >/dev/null 2>&1 || : - /bin/systemctl enable cloud-init-local.service >/dev/null 2>&1 || : fi %else /sbin/chkconfig --add %{_initrddir}/cloud-init-local @@ -139,10 +135,11 @@ fi %if "%{init_system}" == "systemd" if [ $1 -eq 0 ] then + /bin/systemctl --no-reload disable cloud-init-main.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable cloud-init-local.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable cloud-init-network.service >/dev/null 2>&1 || : /bin/systemctl --no-reload disable cloud-config.service >/dev/null 2>&1 || : /bin/systemctl --no-reload disable cloud-final.service >/dev/null 2>&1 || : - /bin/systemctl --no-reload disable cloud-init-network.service >/dev/null 2>&1 || : - /bin/systemctl --no-reload disable cloud-init-local.service >/dev/null 2>&1 || : fi %else if [ $1 -eq 0 ] @@ -189,7 +186,6 @@ fi # Configs %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg -%dir %{_sysconfdir}/cloud/clean.d %dir %{_sysconfdir}/cloud/cloud.cfg.d %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/*.cfg %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/README @@ -198,8 +194,13 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf # Bash completion script +%dir %{_datadir}/bash-completion/completions %{_datadir}/bash-completion/completions/cloud-init +# Man pages +%dir %{_mandir}/man1 +%{_mandir}/man1/*.gz + %{_libexecdir}/%{name} %dir %{_sharedstatedir}/cloud diff --git a/pyproject.toml b/pyproject.toml index e4ba441d7f5..99451a10c67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" +[build-system] # See meson.build. Empty build-system to avoid RTD builds +build-backend = "" +requires = [] [tool.black] line-length = 79 diff --git a/scripts/cloud-id b/scripts/cloud-id new file mode 100644 index 00000000000..553a7f0e21e --- /dev/null +++ b/scripts/cloud-id @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +# Python wrapper for cloud-id command + +import sys + +from cloudinit.cmd import cloud_id + +sys.exit(cloud_id.main()) diff --git a/scripts/cloud-init b/scripts/cloud-init new file mode 100644 index 00000000000..3d3a73063db --- /dev/null +++ b/scripts/cloud-init @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +# Python wrapper for cloud-init CLI + +import sys + +from cloudinit.cmd import main + +sys.exit(main.main()) diff --git a/setup.py b/setup.py deleted file mode 100644 index fd9692e560c..00000000000 --- a/setup.py +++ /dev/null @@ -1,342 +0,0 @@ -# Copyright (C) 2009 Canonical Ltd. -# Copyright (C) 2012 Yahoo! Inc. -# -# Author: Soren Hansen -# Author: Joshua Harlow -# -# This file is part of cloud-init. See LICENSE file for license information. - -# Distutils magic for ec2-init - -import atexit -import os -import platform -import shutil -import subprocess -import sys -import tempfile -from glob import glob - -import setuptools -from setuptools.command.egg_info import egg_info -from setuptools.command.install import install - -# Python-path here is a little unpredictable as setup.py could be run -# from a directory other than the root of the repo, so ensure we can find -# our utils -sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) -# isort: off -from setup_utils import ( # noqa: E402 - get_version, - is_f, - is_generator, - pkg_config_read, - read_requires, -) - -# isort: on -del sys.path[0] - -# pylint: disable=W0402 -try: - from setuptools.errors import DistutilsError -except ImportError: - from distutils.errors import DistutilsArgError as DistutilsError -# pylint: enable=W0402 - -RENDERED_TMPD_PREFIX = "RENDERED_TEMPD" -VARIANT = None -PREFIX = None - - -def render_tmpl(template, mode=None, is_yaml=False): - """render template into a tmpdir under same dir as setup.py - - This is rendered to a temporary directory under the top level - directory with the name 'cloud.cfg'. The reason for not just rendering - to config/cloud.cfg is for a.) don't want to write over contents - in that file if user had something there. b.) debuild will complain - that files are different outside of the debian directory.""" - - # newer versions just use install. - if "install" not in sys.argv: - return template - - tmpl_ext = ".tmpl" - # we may get passed a non-template file, just pass it back - if not template.endswith(tmpl_ext): - return template - - topdir = os.path.dirname(sys.argv[0]) - tmpd = tempfile.mkdtemp(dir=topdir, prefix=RENDERED_TMPD_PREFIX) - atexit.register(shutil.rmtree, tmpd) - bname = os.path.basename(template) - ename, ext = os.path.splitext(bname) - if ext == tmpl_ext: - bname = ename - fpath = os.path.join(tmpd, bname) - cmd_variant = [] - cmd_prefix = [] - if VARIANT: - cmd_variant = ["--variant", VARIANT] - if PREFIX: - cmd_prefix = ["--prefix", PREFIX] - subprocess.run( # nosec B603 - [ - sys.executable, - "./tools/render-template", - *(["--is-yaml"] if is_yaml else []), - *cmd_prefix, - *cmd_variant, - *[template, fpath], - ], - check=True, - ) - if mode: - os.chmod(fpath, mode) - # return path relative to setup.py - return os.path.join(os.path.basename(tmpd), bname) - - -# User can set the variant for template rendering -for a in sys.argv: - if a.startswith("--distro"): - idx = sys.argv.index(a) - if "=" in a: - _, VARIANT = a.split("=") - del sys.argv[idx] - else: - VARIANT = sys.argv[idx + 1] - del sys.argv[idx + 1] - sys.argv.remove("--distro") - -# parse PREFIX and pass it on from render_tmpl() -for a in sys.argv: - if a.startswith("--prefix"): - idx = sys.argv.index(a) - if "=" in a: - _, PREFIX = a.split("=") - else: - PREFIX = sys.argv[idx + 1] - -INITSYS_FILES = { - "sysvinit": lambda: [f for f in glob("sysvinit/redhat/*") if is_f(f)], - "sysvinit_freebsd": lambda: [ - render_tmpl(f, mode=0o755) - for f in glob("sysvinit/freebsd/*") - if is_f(f) - ], - "sysvinit_netbsd": lambda: [ - render_tmpl(f, mode=0o755) - for f in glob("sysvinit/netbsd/*") - if is_f(f) - ], - "sysvinit_openbsd": lambda: [ - render_tmpl(f, mode=0o755) - for f in glob("sysvinit/openbsd/*") - if is_f(f) - ], - "sysvinit_deb": lambda: [f for f in glob("sysvinit/debian/*") if is_f(f)], - "sysvinit_openrc": lambda: [ - f for f in glob("sysvinit/openrc/*") if is_f(f) - ], - "sysvinit_openrc.dep": lambda: ["tools/cloud-init-hotplugd"], - "systemd": lambda: [ - render_tmpl(f) - for f in ( - glob("systemd/*.tmpl") - + glob("systemd/*.service") - + glob("systemd/*.socket") - + glob("systemd/*.target") - ) - if (is_f(f) and not is_generator(f)) - ], - "systemd.generators": lambda: [ - render_tmpl(f, mode=0o755) - for f in glob("systemd/*") - if is_f(f) and is_generator(f) - ], -} -INITSYS_ROOTS = { - "sysvinit": "etc/rc.d/init.d", - "sysvinit_freebsd": "usr/local/etc/rc.d", - "sysvinit_netbsd": "usr/local/etc/rc.d", - "sysvinit_openbsd": "etc/rc.d", - "sysvinit_deb": "etc/init.d", - "sysvinit_openrc": "etc/init.d", - "sysvinit_openrc.dep": "usr/lib/cloud-init", - "systemd": pkg_config_read("systemd", "systemdsystemunitdir"), - "systemd.generators": pkg_config_read( - "systemd", "systemdsystemgeneratordir" - ), -} -INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()]) - - -# Install everything in the right location and take care of Linux (default) and -# FreeBSD systems. -USR = "usr" -ETC = "etc" -USR_LIB_EXEC = "usr/lib" -LIB = "lib" -if os.uname()[0] in ["FreeBSD", "DragonFly", "OpenBSD"]: - USR = "usr/local" - USR_LIB_EXEC = "usr/local/lib" -elif os.path.isfile("/etc/redhat-release"): - USR_LIB_EXEC = "usr/libexec" -elif os.path.isfile("/etc/system-release-cpe"): - with open("/etc/system-release-cpe") as f: - cpe_data = f.read().rstrip().split(":") - (cpe_vendor, cpe_product, cpe_version) = cpe_data[3:6] - if cpe_vendor == "amazon": - USR_LIB_EXEC = "usr/libexec" - - -class MyEggInfo(egg_info): - """This makes sure to not include the rendered files in SOURCES.txt.""" - - def find_sources(self): - egg_info.find_sources(self) - # update the self.filelist. - self.filelist.exclude_pattern( - RENDERED_TMPD_PREFIX + ".*", is_regex=True - ) - # but since mfname is already written we have to update it also. - mfname = os.path.join(self.egg_info, "SOURCES.txt") - if os.path.exists(mfname): - with open(mfname) as fp: - files = [ - f for f in fp if not f.startswith(RENDERED_TMPD_PREFIX) - ] - with open(mfname, "w") as fp: - fp.write("".join(files)) - - -# TODO: Is there a better way to do this?? -class InitsysInstallData(install): - init_system = None - user_options = install.user_options + [ - # This will magically show up in member variable 'init_sys' - ( - "init-system=", - None, - "init system(s) to configure (%s) [default: None]" - % ", ".join(INITSYS_TYPES), - ), - ] - - def initialize_options(self): - install.initialize_options(self) - self.init_system = "" - - def finalize_options(self): - install.finalize_options(self) - - if self.init_system and isinstance(self.init_system, str): - self.init_system = self.init_system.split(",") - - if not self.init_system and not platform.system().endswith("BSD"): - self.init_system = ["systemd"] - - bad = [f for f in self.init_system if f not in INITSYS_TYPES] - if bad: - raise DistutilsError("Invalid --init-system: %s" % ",".join(bad)) - - for system in self.init_system: - # add data files for anything that starts with '.' - datakeys = [ - k for k in INITSYS_ROOTS if k.partition(".")[0] == system - ] - for k in datakeys: - files = INITSYS_FILES[k]() - if not files: - continue - self.distribution.data_files.append((INITSYS_ROOTS[k], files)) - # Force that command to reinitialize (with new file list) - self.distribution.reinitialize_command("install_data", True) - - -USR = "/" + USR -ETC = "/" + ETC -USR_LIB_EXEC = "/" + USR_LIB_EXEC -LIB = "/" + LIB -for k in INITSYS_ROOTS.keys(): - INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k] - -data_files = [ - (ETC + "/cloud", [render_tmpl("config/cloud.cfg.tmpl", is_yaml=True)]), - (ETC + "/cloud/clean.d", glob("config/clean.d/*")), - (ETC + "/cloud/cloud.cfg.d", glob("config/cloud.cfg.d/*")), - (ETC + "/cloud/templates", glob("templates/*")), - ( - USR_LIB_EXEC + "/cloud-init", - [ - "tools/ds-identify", - "tools/hook-hotplug", - "tools/uncloud-init", - "tools/write-ssh-key-fingerprints", - ], - ), - ( - USR + "/share/bash-completion/completions", - ["bash_completion/cloud-init"], - ), - (USR + "/share/doc/cloud-init", [f for f in glob("doc/*") if is_f(f)]), - ( - USR + "/share/doc/cloud-init/examples", - [f for f in glob("doc/examples/*") if is_f(f)], - ), - ( - USR + "/share/doc/cloud-init/examples/seed", - [f for f in glob("doc/examples/seed/*") if is_f(f)], - ), - ( - USR + "/share/doc/cloud-init/module-docs", - [f for f in glob("doc/module-docs/*", recursive=True) if is_f(f)], - ), -] -if not platform.system().endswith("BSD"): - RULES_PATH = pkg_config_read("udev", "udevdir") - RULES_PATH = "/" + RULES_PATH - - data_files.extend( - [ - (RULES_PATH + "/rules.d", [f for f in glob("udev/*.rules")]), - ( - INITSYS_ROOTS["systemd"] + "/sshd-keygen@.service.d/", - ["systemd/disable-sshd-keygen-if-cloud-init-active.conf"], - ), - ] - ) -# Use a subclass for install that handles -# adding on the right init system configuration files -cmdclass = { - "install": InitsysInstallData, - "egg_info": MyEggInfo, -} - -requirements = read_requires() - -setuptools.setup( - name="cloud-init", - version=get_version(), - description="Cloud instance initialization magic", - author="Scott Moser", - author_email="scott.moser@canonical.com", - url="http://launchpad.net/cloud-init/", - package_data={ - "": ["*.json"], - }, - packages=setuptools.find_packages(exclude=["tests.*", "tests"]), - scripts=["tools/cloud-init-per"], - license="Dual-licensed under GPLv3 or Apache 2.0", - data_files=data_files, - install_requires=requirements, - cmdclass=cmdclass, - entry_points={ - "console_scripts": [ - "cloud-init = cloudinit.cmd.main:main", - "cloud-id = cloudinit.cmd.cloud_id:main", - ], - }, -) diff --git a/setup_utils.py b/setup_utils.py deleted file mode 100644 index 476bbd9a4d7..00000000000 --- a/setup_utils.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import subprocess -import sys -from typing import List - - -def is_f(p: str) -> bool: - return os.path.isfile(p) - - -def is_generator(p: str) -> bool: - return "-generator" in p - - -def pkg_config_read(library: str, var: str) -> str: - pkg_config = "pkg-config" - - if os.getenv("PKG_CONFIG"): - pkg_config = os.getenv("PKG_CONFIG") - - fallbacks = { - "systemd": { - "systemdsystemconfdir": "/etc/systemd/system", - "systemdsystemunitdir": "/usr/lib/systemd/system", - "systemdsystemgeneratordir": "/usr/lib/systemd/system-generators", - }, - "udev": { - "udevdir": "/usr/lib/udev", - }, - } - cmd = [pkg_config, f"--variable={var}", library] - try: - path = subprocess.check_output(cmd).decode("utf-8") # nosec B603 - path = path.strip() - except Exception: - path = fallbacks[library][var] - if path.startswith("/"): - path = path[1:] - - return path - - -def version_to_pep440(version: str) -> str: - # read-version can spit out something like 22.4-15-g7f97aee24 - # which is invalid under PEP 440. If we replace the first - with a + - # that should give us a valid version. - return version.replace("-", "+", 1) - - -def get_version() -> str: - cmd = [sys.executable, "tools/read-version"] - ver = subprocess.check_output(cmd) # B603 - version = ver.decode("utf-8").strip() - return version_to_pep440(version) - - -def read_requires() -> List[str]: - cmd = [sys.executable, "tools/read-dependencies"] - deps = subprocess.check_output(cmd) # nosec B603 - return deps.decode("utf-8").splitlines() diff --git a/test-requirements.txt b/test-requirements.txt index c71dddee1e2..c55f9ca4b5e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,9 +11,9 @@ pytest!=7.3.2 pytest-cov pytest-mock pytest-xdist -setuptools jsonschema responses +packaging passlib # This one is currently used only by the CloudSigma and SmartOS datasources. diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py index aaaf92fc583..c467c324895 100644 --- a/tests/unittests/net/test_dhcp.py +++ b/tests/unittests/net/test_dhcp.py @@ -31,6 +31,7 @@ example_netdev, mock, populate_dir, + resourceLocation, ) from tests.unittests.util import MockDistro @@ -1262,7 +1263,7 @@ def _raise(): ), ) def test_parse_raw_lease(self, lease_file, option_245): - lease = load_binary_file(f"tests/data/net/dhcp/{lease_file}") + lease = load_binary_file(resourceLocation(f"net/dhcp/{lease_file}")) assert option_245 == Dhcpcd.parse_unknown_options_from_packet( lease, 245 ) diff --git a/tools/run-container b/tools/run-container index 5c5d3577631..5a14140af2e 100755 --- a/tools/run-container +++ b/tools/run-container @@ -24,7 +24,7 @@ Usage: ${0##*/} [ options ] [images:]image-ref To see images available, run 'lxc image list images:' Example input: - centos/7 + rockylinux/9 opensuse/15.4 debian/10 diff --git a/tools/test_tools.py b/tools/test_tools.py index 694aed61427..38440758e9d 100644 --- a/tools/test_tools.py +++ b/tools/test_tools.py @@ -1,23 +1,10 @@ import pathlib +from packaging.version import Version, InvalidVersion from importlib.machinery import SourceFileLoader from importlib.util import module_from_spec, spec_from_loader from unittest import mock import pytest -import setuptools - - -from setup_utils import version_to_pep440 # pylint: disable=import-error - -try: - validate_version = setuptools.dist.Distribution._validate_version # type: ignore # noqa: E501 - setuptools.sic # pylint: disable=no-member,pointless-statement -except AttributeError: - pytest.skip( - "Unable to import necessary setuptools utilities. " - "Version is likely too old.", - allow_module_level=True, - ) # Since read-version has a '-' and no .py extension, we have to do this # to import it @@ -36,9 +23,17 @@ spec.loader.exec_module(read_version) +def version_to_pep440(version: str) -> str: + # read-version can spit out something like 22.4-15-g7f97aee24 + # which is invalid under PEP 440. If we replace the first - with a + + # that should give us a valid version. + return version.replace("-", "+", 1) + + def assert_valid_version(version): - response = validate_version(version) - if isinstance(response, setuptools.sic): # pylint: disable=no-member + try: + Version(version) + except InvalidVersion: pytest.fail(f"{version} is not PEP 440 compliant") diff --git a/tox.ini b/tox.ini index c8feba0c262..3c5c716cb84 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,6 @@ deps = types-passlib types-PyYAML types-requests - types-setuptools typing-extensions [pinned_versions] @@ -186,7 +185,6 @@ deps = pytest==4.6.9 pytest-cov==2.8.1 pytest-mock==1.10.4 - setuptools==45.2.0 responses==0.9.0 passlib # required for this version of jinja2 From a5d2809960a9d6551e94450fd7ed22d6647cd39e Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 24 Jul 2025 12:31:40 -0600 Subject: [PATCH 094/150] fix: add apt-get update before development pkg install (#6326) --- tools/read-dependencies | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/read-dependencies b/tools/read-dependencies index 934c88aa9e6..2dd0b251b88 100755 --- a/tools/read-dependencies +++ b/tools/read-dependencies @@ -115,6 +115,12 @@ DISTRO_INSTALL_PKG_CMD = { "suse": ZYPPER_INSTALL, } +DISTRO_UPDATE_PKG_CMD = { + "redhat": ["yum", "update"], + "debian": ["apt", "update", "-q"], + "suse": ["zypper", "update"], +} + # List of base system packages required to enable ci automation CI_SYSTEM_BASE_PKGS = { "common": ["make", "sudo", "tar"], @@ -347,19 +353,27 @@ def pkg_install(pkg_list, distro, test_distro=False, dry_run=False): "(dryrun)" if dry_run else "", " ".join(pkg_list) ) ) + distro_family = DISTRO_PKG_TYPE_MAP[distro] install_cmd = [] + update_cmd = DISTRO_UPDATE_PKG_CMD.get(distro_family, []) if dry_run: install_cmd.append("echo") + if update_cmd: + update_cmd.insert(0, "echo") if os.geteuid() != 0: install_cmd.append("sudo") + if update_cmd: + update_cmd.append("sudo") - distro_family = DISTRO_PKG_TYPE_MAP[distro] if dry_run and distro_family in DRYRUN_DISTRO_INSTALL_PKG_CMD: cmd = DRYRUN_DISTRO_INSTALL_PKG_CMD[distro_family] else: cmd = DISTRO_INSTALL_PKG_CMD[distro_family] install_cmd.extend(cmd) + if update_cmd: + subprocess.check_call(update_cmd) + if distro in ["centos", "redhat", "rocky", "eurolinux"]: # CentOS and Redhat need epel-release to access oauthlib and jsonschema subprocess.check_call(install_cmd + ["epel-release"]) From ff9a69ba106f8658635c2261e602a395f2791da6 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 4 Aug 2025 11:05:40 -0600 Subject: [PATCH 095/150] chore: update debian developer packaging template (#6326) --- packages/debian/control.in | 90 +++++++++++++++++++++++++++++++++----- packages/debian/rules | 12 ++--- packages/pkg-deps.json | 9 +++- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/packages/debian/control.in b/packages/debian/control.in index 87403fe91bc..2ce4fe2d7dd 100644 --- a/packages/debian/control.in +++ b/packages/debian/control.in @@ -2,20 +2,86 @@ Source: cloud-init Section: admin Priority: optional -Maintainer: Scott Moser +Homepage: https://cloud-init.io/ +Maintainer: Ubuntu Developers Build-Depends: ${build_depends} XS-Python-Version: all -Standards-Version: 3.9.6 +Vcs-Browser: https://github.com/canonical/cloud-init/tree/main +Vcs-Git: https://github.com/canonical/cloud-init -b main +Standards-Version: 4.5.0 +Rules-Requires-Root: no -Package: cloud-init +Package: cloud-init-base Architecture: all -Depends: ${misc:Depends}, - ${python3:Depends}, +Depends: cloud-guest-utils | cloud-utils, + dhcpcd-base, iproute2, - python3-debconf -Breaks: cloud-init-base -Recommends: eatmydata, sudo, software-properties-common, gdisk -Suggests: ssh-import-id, openssh-server -Description: Init scripts for cloud instances - Cloud instances need special scripts to run during initialization - to retrieve and install ssh keys and to let the user run various scripts. + netcat-openbsd, + netplan.io, + procps, + python3, + python3-debconf, + python3-requests, + ${misc:Depends}, + ${python3:Depends} +Recommends: eatmydata, gdisk, gnupg, python3-apt, software-properties-common +Suggests: openssh-server, ssh-import-id +Replaces: cloud-init (<< 25.1~), cloud-init-base +Breaks: cloud-init (<< 25.1~), cloud-init-base +Description: initialization and customization tool for cloud instances + Cloud-init with minimal dependencies, refer to cloud-init for more + information. + +Package: cloud-init-azure +Architecture: all +Depends: cloud-init-base, + python3-passlib, + ${misc:Depends}, +Description: Azure specific cloud-init + This metapackage depends on cloud-init-base and additional packages for + Azure. + +Package: cloud-init-cloud-sigma +Architecture: all +Depends: cloud-init-base, + python3-serial, + ${misc:Depends}, +Description: Cloud Sigma specific cloud-init + This metapackage depends on cloud-init-base and additional packages for + Cloud Sigma. + +Package: cloud-init-smart-os +Architecture: all +Depends: cloud-init-base, + python3-serial, + ${misc:Depends}, +Description: Smart OS specific cloud-init + This metapackage depends on cloud-init-base and additional packages for + Smart OS. + +Package: cloud-init +Architecture: all +Depends: cloud-init-base, + python3-serial, + python3-passlib, + ${misc:Depends}, +Description: initialization and customization tool for cloud instances + Cloud-init is the industry standard multi-distribution method for + cross-platform cloud instance initialization. It is supported across all major + public cloud providers, provisioning systems for private cloud infrastructure, + and bare-metal installations. + . + Cloud instances are initialized from a disk image and instance data: + . + * Cloud metadata + * User data (optional) + * Vendor data (optional) + . + Cloud-init will identify the cloud it is running on during boot, read any + provided metadata from the cloud and initialize the system accordingly. This + may involve setting up the network and storage devices to configuring SSH + access key and many other aspects of a system. Later on the cloud-init will + also parse and process any optional user or vendor data that was passed to + the instance. + . + This is a metapackage that includes every cloud-init dependency. diff --git a/packages/debian/rules b/packages/debian/rules index bfd2779ba74..d314400bc21 100755 --- a/packages/debian/rules +++ b/packages/debian/rules @@ -2,11 +2,13 @@ include /usr/share/dpkg/pkg-info.mk +export PYTHONDONTWRITEBYTECODE=1 + %: dh $@ --buildsystem meson override_dh_auto_configure: - dh_auto_configure -- -Dinit_system=systemd -Dlibexecdir=lib + dh_auto_configure -- -Dinit_system=systemd -Dlibexecdir=lib -Ddistro_templates=chef_client.rb.tmpl,chrony.conf.ubuntu.tmpl,hosts.debian.tmpl,ntp.conf.ubuntu.tmpl,sources.list.ubuntu.deb822.tmpl,sources.list.ubuntu.deb822.tmpl,timesyncd.conf.tmpl override_dh_auto_test: @@ -20,8 +22,8 @@ override_dh_installsystemd: dh_installsystemd --no-restart-on-upgrade --no-start override_dh_auto_install: - dh_auto_install --destdir=debian/cloud-init - install -D -m 0644 ./tools/21-cloudinit.conf debian/cloud-init/etc/rsyslog.d/21-cloudinit.conf - install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init/etc/profile.d/Z99-cloud-locale-test.sh - install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init/etc/profile.d/Z99-cloudinit-warnings.sh + dh_auto_install --destdir=debian/cloud-init-base + install -D -m 0644 ./tools/21-cloudinit.conf debian/cloud-init-base/etc/rsyslog.d/21-cloudinit.conf + install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init-base/etc/profile.d/Z99-cloud-locale-test.sh + install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init-base/etc/profile.d/Z99-cloudinit-warnings.sh flist=$$(find $(CURDIR)/debian/ -type f -name version.py) && sed -i 's,@@PACKAGED_VERSION@@,$(DEB_VERSION),' $${flist:-did-not-find-version-py-for-replacement} diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json index bb3ecf12ede..6bf142c6e0b 100644 --- a/packages/pkg-deps.json +++ b/packages/pkg-deps.json @@ -5,13 +5,20 @@ "pkgconf", "bash-completion", "debhelper", - "systemd-dev" + "systemd-dev", + "python3", + "python3-debconf" ], "renames" : { "pyyaml" : "python3-yaml", "pyserial" : "python3-serial" }, "requires" : [ + "debconf", + "dhcpcd-base", + "iproute2", + "netcat-openbsd", + "netplan.io", "procps" ] }, From ad367e2b1d962d5e1f90eb14bcea06de4da9a7f4 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 4 Aug 2025 11:07:00 -0600 Subject: [PATCH 096/150] chore: drop yaml validation target, covered in unittests (#6326) - tests/unittests/config/test_schema.py::TestModuleDocs and TestCloudConfigExamples - tests/unittests/test_merging.py --- Makefile | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 1eebb048efc..ab0b02cff0a 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,6 @@ CWD=$(shell pwd) VARIANT ?= ubuntu -YAML_FILES=$(shell find cloudinit tests tools -name "*.yaml" -type f ) -YAML_FILES+=$(shell find doc/examples -name "cloud-config*.txt" -type f ) - PYTHON ?= python3 NUM_ITER ?= 100 @@ -19,7 +16,7 @@ BENCHMARK=./tools/benchmark.sh all: check -check: check_version test yaml +check: check_version test style-check: lint @@ -93,9 +90,6 @@ clean_release: clean: clean_pyc clean_pytest clean_packaging clean_release rm -rf doc/rtd_html .tox .coverage tags $(GENERATOR_F) -yaml: - @$(PYTHON) $(CWD)/tools/validate-yaml.py $(YAML_FILES) - rpm: $(PYTHON) ./packages/brpm --distro=$(distro) @@ -125,7 +119,7 @@ fmt-tip: tox -e do_format_tip && tox -e check_format_tip -.PHONY: all check test lint clean rpm srpm deb deb-src yaml +.PHONY: all check test lint clean rpm srpm deb deb-src .PHONY: check_version clean_pyc .PHONY: unittest style-check render-template benchmark-generator .PHONY: clean_pytest clean_packaging clean_release doc From 232e8c18f7b297d780e77bac7618058b2a489763 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 4 Aug 2025 13:47:27 -0600 Subject: [PATCH 097/150] chore: update developer specfile redhat/fedora and dependencies (#6326) --- packages/pkg-deps.json | 19 +++++++ packages/redhat/cloud-init.spec.in | 87 ++++-------------------------- tools/read-dependencies | 4 +- 3 files changed, 31 insertions(+), 79 deletions(-) diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json index 6bf142c6e0b..306ef4f850a 100644 --- a/packages/pkg-deps.json +++ b/packages/pkg-deps.json @@ -69,6 +69,25 @@ "hostname" ] }, + "fedora": { + "build-requires" : [ + "bash-completion-devel", + "meson", + "pkgconf", + "python3-devel", + "systemd-devel" + ], + "requires" : [ + "e2fsprogs", + "iproute", + "net-tools", + "procps", + "rsyslog", + "shadow-utils", + "sudo", + "hostname" + ] + }, "suse" : { "renames" : { "jinja2" : "python3-Jinja2", diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in index c598d722b9f..7d91d2d8440 100644 --- a/packages/redhat/cloud-init.spec.in +++ b/packages/redhat/cloud-init.spec.in @@ -1,11 +1,4 @@ ## template: jinja -%define use_systemd (0%{?fedora} && 0%{?fedora} >= 18) || (0%{?rhel} && 0%{?rhel} >= 7) - -%if %{use_systemd} -%define init_system systemd -%else -%define init_system sysvinit -%endif # See: http://www.zarb.org/~jasonc/macros.php # Or: http://fedoraproject.org/wiki/Packaging:ScriptletSnippets @@ -14,7 +7,7 @@ Name: cloud-init Version: {{rpm_upstream_version}} Release: 1{{subrelease}}%{?dist} -Summary: Cloud instance initilization tool +Summary: Cloud instance initialization tool Group: System Environment/Base License: Dual-licesed GPLv3 or Apache 2.0 @@ -24,20 +17,10 @@ Source0: {{archive_name}} BuildArch: noarch BuildRoot: %{_tmppath} -%if "%{?el6}" == "1" -BuildRequires: python-argparse -%endif -%if %{use_systemd} -Requires: systemd -BuildRequires: systemd -Requires: systemd-units -BuildRequires: systemd-units -%else -Requires: initscripts >= 8.36 -Requires(postun): initscripts -Requires(post): chkconfig -Requires(preun): chkconfig -%endif +Requires: systemd +BuildRequires: pkgconfig(systemd) +Requires: systemd-units +BuildRequires: systemd-units {% for r in buildrequires %} BuildRequires: {{r}} @@ -61,16 +44,9 @@ Requires: {{r}} Patch{{loop.index0}}: {{p}} {% endfor %} -%if "%{init_system}" == "systemd" Requires(post): systemd Requires(preun): systemd Requires(postun): systemd -%else -Requires(post): chkconfig -Requires(postun): initscripts -Requires(preun): chkconfig -Requires(preun): initscripts -%endif %description Cloud-init is a set of init scripts for cloud instances. Cloud instances @@ -86,7 +62,7 @@ ssh keys and to let the user run various scripts. {% endfor %} %build -%meson -Dinit_system=systemd +%meson -Dinit_system=systemd -Ddistro_templates=chef_client.rb.tmpl,chrony.conf.rhel.tmpl,hosts.redhat.tmpl,ntp.conf.rhel.tmpl,resolv.conf.tmpl,timesyncd.conf.tmpl %meson_build %install @@ -113,68 +89,23 @@ version_pys=$(cd "$RPM_BUILD_ROOT" && find . -name version.py -type f) rm -rf $RPM_BUILD_ROOT %post +%systemd_post cloud-init-main.service cloud-config.service cloud-config.target cloud-final.service cloud-init-network.service cloud-init.target cloud-init-local.service -%if "%{init_system}" == "systemd" -if [ $1 -eq 1 ] -then - /bin/systemctl enable cloud-init-main.service >/dev/null 2>&1 || : - /bin/systemctl enable cloud-init-local.service >/dev/null 2>&1 || : - /bin/systemctl enable cloud-init-network.service >/dev/null 2>&1 || : - /bin/systemctl enable cloud-config.service >/dev/null 2>&1 || : - /bin/systemctl enable cloud-final.service >/dev/null 2>&1 || : -fi -%else -/sbin/chkconfig --add %{_initrddir}/cloud-init-local -/sbin/chkconfig --add %{_initrddir}/cloud-init -/sbin/chkconfig --add %{_initrddir}/cloud-config -/sbin/chkconfig --add %{_initrddir}/cloud-final -%endif %preun +%systemd_preun cloud-init-main.service cloud-config.service cloud-config.target cloud-final.service cloud-init-network.service cloud-init.target cloud-init-local.service -%if "%{init_system}" == "systemd" -if [ $1 -eq 0 ] -then - /bin/systemctl --no-reload disable cloud-init-main.service >/dev/null 2>&1 || : - /bin/systemctl --no-reload disable cloud-init-local.service >/dev/null 2>&1 || : - /bin/systemctl --no-reload disable cloud-init-network.service >/dev/null 2>&1 || : - /bin/systemctl --no-reload disable cloud-config.service >/dev/null 2>&1 || : - /bin/systemctl --no-reload disable cloud-final.service >/dev/null 2>&1 || : -fi -%else -if [ $1 -eq 0 ] -then - /sbin/service cloud-init stop >/dev/null 2>&1 || : - /sbin/chkconfig --del cloud-init || : - /sbin/service cloud-init-local stop >/dev/null 2>&1 || : - /sbin/chkconfig --del cloud-init-local || : - /sbin/service cloud-config stop >/dev/null 2>&1 || : - /sbin/chkconfig --del cloud-config || : - /sbin/service cloud-final stop >/dev/null 2>&1 || : - /sbin/chkconfig --del cloud-final || : -fi -%endif %postun - -%if "%{init_system}" == "systemd" -/bin/systemctl daemon-reload >/dev/null 2>&1 || : -%endif +%systemd_postun cloud-init-main.service cloud-config.service cloud-config.target cloud-final.service cloud-init-network.service cloud-init.target cloud-init-local.service %files %{_udevrulesdir}/66-azure-ephemeral.rules -%if "%{init_system}" == "systemd" /usr/lib/systemd/system-generators/cloud-init-generator /usr/lib/systemd/system/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf %{_unitdir}/cloud-* -%else -%attr(0755, root, root) %{_initddir}/cloud-config -%attr(0755, root, root) %{_initddir}/cloud-final -%attr(0755, root, root) %{_initddir}/cloud-init-local -%attr(0755, root, root) %{_initddir}/cloud-init -%endif # Program binaries %{_bindir}/cloud-init* diff --git a/tools/read-dependencies b/tools/read-dependencies index 2dd0b251b88..de88b85afac 100755 --- a/tools/read-dependencies +++ b/tools/read-dependencies @@ -26,7 +26,7 @@ DISTRO_PKG_TYPE_MAP = { "centos": "redhat", "eurolinux": "redhat", "miraclelinux": "redhat", - "fedora": "redhat", + "fedora": "fedora", "rocky": "redhat", "redhat": "redhat", "debian": "debian", @@ -107,10 +107,12 @@ ZYPPER_INSTALL = [ DRYRUN_DISTRO_INSTALL_PKG_CMD = { "redhat": ["yum", "install", "--assumeyes"], + "fedora": ["yum", "install", "--assumeyes"], } DISTRO_INSTALL_PKG_CMD = { "redhat": MAYBE_RELIABLE_YUM_INSTALL, + "fedora": MAYBE_RELIABLE_YUM_INSTALL, "debian": ["apt", "install", "-y"], "suse": ZYPPER_INSTALL, } From 6979999625d4e91d70f98990dd98d934a1da507d Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 5 Aug 2025 15:10:28 -0600 Subject: [PATCH 098/150] chore: update developer specfile and build for opensuse (#6326) tools/read-dependencies DISTRO_PKG_MAP values are only one of ubuntu, redhat, suse or fedora. Simplify value check when adding rpm-build package dependency. --- packages/pkg-deps.json | 6 ++++-- packages/suse/cloud-init.spec.in | 24 +++++++----------------- tools/read-dependencies | 10 +--------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json index 306ef4f850a..86c2871442c 100644 --- a/packages/pkg-deps.json +++ b/packages/pkg-deps.json @@ -94,10 +94,12 @@ "pyyaml" : "python3-PyYAML" }, "build-requires" : [ + "meson", + "pkgconf", + "bash-completion-devel", "fdupes", "filesystem", - "python3-devel", - "python3-setuptools" + "python3-devel" ], "requires" : [ "iproute2", diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in index fae3c12b294..1e063243d97 100644 --- a/packages/suse/cloud-init.spec.in +++ b/packages/suse/cloud-init.spec.in @@ -48,25 +48,15 @@ end for {% endfor %} %build -%{__python} setup.py build +%meson -Dinit_system=systemd --libexecdir=/usr/lib -Ddistro_templates=chef_client.rb.tmpl,chrony.conf.opensuse.tmpl,hosts.suse.tmpl,ntp.conf.opensuse.tmpl,resolv.conf.tmpl,timesyncd.conf.tmpl +%meson_build %install -%{__python} setup.py install \ - --skip-build --root=%{buildroot} --prefix=%{_prefix} \ - --record-rpm=INSTALLED_FILES --install-lib=%{python_sitelib} \ - --init-system=systemd - -# Move udev rules -mkdir -p %{buildroot}/usr/lib/udev/rules.d/ -mv %{buildroot}/lib/udev/rules.d/* %{buildroot}/usr/lib/udev/rules.d/ - -# Remove non-SUSE templates -rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.debian.* -rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.redhat.* -rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.ubuntu.* +%meson_install # Move documentation mkdir -p %{buildroot}/%{_defaultdocdir} +mkdir -p %{buildroot}/%{_sysconfdir}/cloud/clean.d mv %{buildroot}/usr/share/doc/cloud-init %{buildroot}/%{_defaultdocdir} for doc in LICENSE ChangeLog requirements.txt; do cp ${doc} %{buildroot}/%{_defaultdocdir}/cloud-init @@ -102,6 +92,7 @@ version_pys=$(cd "%{buildroot}" && find . -name version.py -type f) # There doesn't seem to be an agreed upon place for these # although it appears the standard says /usr/lib but rpmbuild # will try /usr/lib64 ?? +/usr/lib/%{name}/hook-hotplug /usr/lib/%{name}/uncloud-init /usr/lib/%{name}/write-ssh-key-fingerprints /usr/lib/%{name}/ds-identify @@ -114,7 +105,7 @@ version_pys=$(cd "%{buildroot}" && find . -name version.py -type f) %doc %{_defaultdocdir}/cloud-init/* # Configs -%dir %{_sysconfdir}/cloud/clean.d +%dir %{_sysconfdir}/cloud/clean.d %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg %dir %{_sysconfdir}/cloud/cloud.cfg.d %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/*.cfg @@ -124,8 +115,7 @@ version_pys=$(cd "%{buildroot}" && find . -name version.py -type f) # Bash completion script %{_datadir}/bash-completion/completions/cloud-init - -%{_sysconfdir}/systemd/system/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf +/usr/share/man/man1/cloud-*gz # Python code is here... %{python_sitelib}/* diff --git a/tools/read-dependencies b/tools/read-dependencies index de88b85afac..65620185e78 100755 --- a/tools/read-dependencies +++ b/tools/read-dependencies @@ -387,15 +387,7 @@ def pkg_install(pkg_list, distro, test_distro=False, dry_run=False): subprocess.call( ["dnf", "config-manager", "--set-disabled", "epel-cisco-openh264"] ) - if distro in [ - "suse", - "opensuse", - "fedora", - "redhat", - "rocky", - "centos", - "eurolinux", - ]: + if DISTRO_PKG_TYPE_MAP.get(distro) in ["redhat", "suse", "fedora"]: pkg_list.append("rpm-build") subprocess.check_call(install_cmd + pkg_list) From fed4375697d8e5e5282cd456c74fb43af12d23d1 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 15 Aug 2025 16:52:19 -0600 Subject: [PATCH 099/150] doc: add testing links to doc/rtd Development headings (#6326) Migrate package testing build info into existing testing page. --- doc/rtd/development/contribute_testing.rst | 8 +++++ doc/rtd/development/package_testing.rst | 26 ++++++++++++++++ doc/rtd/development/packaging.rst | 31 +++---------------- .../ubuntu_test_prerelease.rst | 0 doc/rtd/howto/index.rst | 1 - doc/rtd/index.rst | 2 +- 6 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 doc/rtd/development/contribute_testing.rst create mode 100644 doc/rtd/development/package_testing.rst rename doc/rtd/{howto => development}/ubuntu_test_prerelease.rst (100%) diff --git a/doc/rtd/development/contribute_testing.rst b/doc/rtd/development/contribute_testing.rst new file mode 100644 index 00000000000..ec7ec5dab45 --- /dev/null +++ b/doc/rtd/development/contribute_testing.rst @@ -0,0 +1,8 @@ +Contribute to testing +********************* + +.. toctree:: + :maxdepth: 1 + + package_testing.rst + ubuntu_test_prerelease.rst diff --git a/doc/rtd/development/package_testing.rst b/doc/rtd/development/package_testing.rst new file mode 100644 index 00000000000..9097731dbc0 --- /dev/null +++ b/doc/rtd/development/package_testing.rst @@ -0,0 +1,26 @@ +.. _package_testing: + +Development package builds +************************** + +To ease the development and testing of local packaging changes, +development-quality DEB or RPM packages can be built with one of the following +scripts on a build host which already has all system build dependencies +installed: + +.. code-block:: bash + + ./packages/brpm --distro=redhat # or --distro=suse to build an RPM + ./packages/bddeb -d # to build a DEB + +OR if LXD is present, the full package build can be run in a container: + +.. code-block:: bash + + ./tools/run-container ubuntu-daily:plucky --package --keep + ./tools/run-container rockylinux/9 --package --keep + + +.. note:: + + meson support has not yet been added to the BSDs in :file:`tools/build-on-*bsd` or :file:`meson.build`. diff --git a/doc/rtd/development/packaging.rst b/doc/rtd/development/packaging.rst index d5ee1b88c7b..b366dfdf65b 100644 --- a/doc/rtd/development/packaging.rst +++ b/doc/rtd/development/packaging.rst @@ -28,12 +28,12 @@ Additional dependencies for systemd environments: - ``systemd-devel`` - ``udev`` -The full list of all package build-dependencies for a given -distribution can be obtained by the following command: +The full list of all package build dependencies for a given distribution can +be obtained by the following command: .. code-block:: bash - ./tools/read-dependencies --system --distro= + ./tools/read-dependencies --requirements-file requirements.txt --requirements-file test-requirements.txt --system-pkg-names --system-pkg-names --distro= Manual build procedure @@ -52,30 +52,7 @@ Steps to validate ``cloud-init`` package builds in a development environment: # List installed files find builddir/testinstall/ - -Test builds of RPMs or DEBs ---------------------------- -To ease the development and testing of local changes, development-quality DEB -or RPM packages can be built with one of the following scripts on a build host -which already has all system build dependencies installed: - -.. code-block:: bash - - ./packages/brpm --distro=redhat # or --distro=suse to build an RPM - ./packages/bddeb -d # to build a DEB - -OR if LXC is present, the full package build can be run in a container: - -.. code-block:: bash - - ./tools/run-container ubuntu-daily:plucky --package --keep - ./tools/run-container rockylinux/9 --package --keep - - -.. note:: - - meson support has not yet been added to the BSDs in :file:`tools/build-on-*bsd` or :file:`meson.build`. - +See :ref:`package_testing` for more information package testing. .. LINKS: .. _meson: https://mesonbuild.com/ diff --git a/doc/rtd/howto/ubuntu_test_prerelease.rst b/doc/rtd/development/ubuntu_test_prerelease.rst similarity index 100% rename from doc/rtd/howto/ubuntu_test_prerelease.rst rename to doc/rtd/development/ubuntu_test_prerelease.rst diff --git a/doc/rtd/howto/index.rst b/doc/rtd/howto/index.rst index d03e3909f66..096a14b2b28 100644 --- a/doc/rtd/howto/index.rst +++ b/doc/rtd/howto/index.rst @@ -28,4 +28,3 @@ How do I...? Report a bug Identify my datasource Disable cloud-init - Test pre-release cloud-init on Ubuntu diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst index 9d1f1543a54..6a3ef84684c 100644 --- a/doc/rtd/index.rst +++ b/doc/rtd/index.rst @@ -98,10 +98,10 @@ projects, contributions, suggestions, fixes and constructive feedback. Contributing overview Contribute to code Contribute to docs + Contribute to testing Community Downstream packaging - .. LINKS .. include:: links.txt .. _Release schedule: https://discourse.ubuntu.com/t/2025-cloud-init-release-schedule/55534 From 890873f50d7f14dc235f5eaf2820aae3288a98b1 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 21 Aug 2025 09:58:26 -0600 Subject: [PATCH 100/150] fix: read-dependencies syntax error on regex (#6326) --- tools/read-dependencies | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/read-dependencies b/tools/read-dependencies index 65620185e78..8f58908df5e 100755 --- a/tools/read-dependencies +++ b/tools/read-dependencies @@ -67,13 +67,13 @@ MAYBE_RELIABLE_YUM_INSTALL = [ grep -q baseurl $CENTOS_REPO if [ $? -eq 1 ]; then # CentOS 9 does not provide baseurl definitions - sed -i '/\[baseos\]/a baseurl=https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os' ${CENTOS_REPO} - sed -i '/\[appstream\]/a baseurl=https://mirror.stream.centos.org/9-stream/AppStream/x86_64/os' ${CENTOS_REPO} - sed -i '/\[crb\]/a baseurl=https://mirror.stream.centos.org/9-stream/CRB/x86_64/os' ${CENTOS_REPO} + sed -i '/\\[baseos\\]/a baseurl=https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os' ${CENTOS_REPO} + sed -i '/\\[appstream\\]/a baseurl=https://mirror.stream.centos.org/9-stream/AppStream/x86_64/os' ${CENTOS_REPO} + sed -i '/\\[crb\\]/a baseurl=https://mirror.stream.centos.org/9-stream/CRB/x86_64/os' ${CENTOS_REPO} fi CENTOS_EXTRAS_REPO="/etc/yum.repos.d/centos-addons.repo" if [ -f $CENTOS_EXTRAS_REPO ]; then - sed -i '/\[extras-common\]/a baseurl=https://mirror.stream.centos.org/SIGs/9-stream/extras/x86_64/extras-common' ${CENTOS_EXTRAS_REPO} + sed -i '/\\[extras-common\\]/a baseurl=https://mirror.stream.centos.org/SIGs/9-stream/extras/x86_64/extras-common' ${CENTOS_EXTRAS_REPO} fi fi } From 21cc50c6d2d6626d2b7047be04350f797c089446 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Tue, 26 Aug 2025 10:50:29 +0200 Subject: [PATCH 101/150] test: drop citestcase from test_version.py (#6415) --- tests/unittests/test_version.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/unittests/test_version.py b/tests/unittests/test_version.py index 52a8ae33527..f4e616addb4 100644 --- a/tests/unittests/test_version.py +++ b/tests/unittests/test_version.py @@ -3,27 +3,26 @@ from unittest import mock from cloudinit import version -from tests.unittests.helpers import CiTestCase -class TestExportsFeatures(CiTestCase): +class TestExportsFeatures: def test_has_network_config_v1(self): - self.assertIn("NETWORK_CONFIG_V1", version.FEATURES) + assert "NETWORK_CONFIG_V1" in version.FEATURES def test_has_network_config_v2(self): - self.assertIn("NETWORK_CONFIG_V2", version.FEATURES) + assert "NETWORK_CONFIG_V2" in version.FEATURES -class TestVersionString(CiTestCase): +class TestVersionString: @mock.patch( "cloudinit.version._PACKAGED_VERSION", "17.2-3-gb05b9972-0ubuntu1" ) def test_package_version_respected(self): """If _PACKAGED_VERSION is filled in, then it should be returned.""" - self.assertEqual("17.2-3-gb05b9972-0ubuntu1", version.version_string()) + assert "17.2-3-gb05b9972-0ubuntu1" == version.version_string() @mock.patch("cloudinit.version._PACKAGED_VERSION", "@@PACKAGED_VERSION@@") @mock.patch("cloudinit.version.__VERSION__", "17.2") def test_package_version_skipped(self): """If _PACKAGED_VERSION is not modified, then return __VERSION__.""" - self.assertEqual("17.2", version.version_string()) + assert "17.2" == version.version_string() From d3240b1685ae4a018f1920b2d77ddc7236c5f95c Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Tue, 26 Aug 2025 10:53:16 +0200 Subject: [PATCH 102/150] test: drop citestcase from net/test_init.py (#6410) --- tests/unittests/net/test_init.py | 257 ++++++++++++++----------------- 1 file changed, 117 insertions(+), 140 deletions(-) diff --git a/tests/unittests/net/test_init.py b/tests/unittests/net/test_init.py index af43a87c765..7bbaae1e392 100644 --- a/tests/unittests/net/test_init.py +++ b/tests/unittests/net/test_init.py @@ -17,7 +17,11 @@ 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, example_netdev, random_string +from tests.unittests.helpers import ( + assert_count_equal, + example_netdev, + random_string, +) from tests.unittests.util import MockDistro @@ -223,26 +227,20 @@ def test_is_vlan(self): assert net.is_vlan("eth0") -class TestGenerateFallbackConfig(CiTestCase): - def setUp(self): - super(TestGenerateFallbackConfig, 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) - self.add_patch( - "cloudinit.net.util.is_container", - "m_is_container", - return_value=False, +class TestGenerateFallbackConfig: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + self.sysdir = str(tmp_path) + "/" + mocker.patch( + "cloudinit.net.get_sys_class_path", return_value=self.sysdir ) - self.add_patch("cloudinit.net.util.udevadm_settle", "m_settle") - self.add_patch( - "cloudinit.net.is_netfailover", "m_netfail", return_value=False + mocker.patch("cloudinit.net.util.is_container", return_value=False) + mocker.patch("cloudinit.net.util.udevadm_settle") + self.m_is_netfail = mocker.patch( + "cloudinit.net.is_netfailover", return_value=False ) - self.add_patch( + mocker.patch( "cloudinit.net.is_netfail_master", - "m_netfail_master", return_value=False, ) @@ -263,7 +261,7 @@ def test_generate_fallback_finds_connected_eth_with_mac(self): }, "version": 2, } - self.assertEqual(expected, net.generate_fallback_config()) + assert expected == net.generate_fallback_config() def test_generate_fallback_finds_dormant_eth_with_mac(self): """generate_fallback_config finds any dormant device with a mac.""" @@ -281,7 +279,7 @@ def test_generate_fallback_finds_dormant_eth_with_mac(self): }, "version": 2, } - self.assertEqual(expected, net.generate_fallback_config()) + assert expected == net.generate_fallback_config() def test_generate_fallback_finds_eth_by_operstate(self): """generate_fallback_config finds any dormant device with a mac.""" @@ -301,15 +299,15 @@ def test_generate_fallback_finds_eth_by_operstate(self): valid_operstates = ["dormant", "down", "lowerlayerdown", "unknown"] for state in valid_operstates: write_file(os.path.join(self.sysdir, "eth0", "operstate"), state) - self.assertEqual(expected, net.generate_fallback_config()) + assert expected == net.generate_fallback_config() write_file(os.path.join(self.sysdir, "eth0", "operstate"), "noworky") - self.assertIsNone(net.generate_fallback_config()) + assert net.generate_fallback_config() is None def test_generate_fallback_config_skips_veth(self): """generate_fallback_config will skip any veth interfaces.""" # A connected veth which gets ignored write_file(os.path.join(self.sysdir, "veth0", "carrier"), "1") - self.assertIsNone(net.generate_fallback_config()) + assert net.generate_fallback_config() is None def test_generate_fallback_config_skips_bridges(self): """generate_fallback_config will skip any bridges interfaces.""" @@ -318,7 +316,7 @@ def test_generate_fallback_config_skips_bridges(self): mac = "aa:bb:cc:aa:bb:cc" write_file(os.path.join(self.sysdir, "eth0", "address"), mac) ensure_file(os.path.join(self.sysdir, "eth0", "bridge")) - self.assertIsNone(net.generate_fallback_config()) + assert net.generate_fallback_config() is None def test_generate_fallback_config_skips_bonds(self): """generate_fallback_config will skip any bonded interfaces.""" @@ -327,9 +325,12 @@ def test_generate_fallback_config_skips_bonds(self): mac = "aa:bb:cc:aa:bb:cc" write_file(os.path.join(self.sysdir, "eth0", "address"), mac) ensure_file(os.path.join(self.sysdir, "eth0", "bonding")) - self.assertIsNone(net.generate_fallback_config()) + assert net.generate_fallback_config() is None - def test_generate_fallback_config_skips_netfail_devs(self): + @mock.patch("cloudinit.net.is_netfail_master") + def test_generate_fallback_config_skips_netfail_devs( + self, m_is_netfail_master + ): """gen_fallback_config ignores netfail primary,sby no mac on master.""" mac = "aa:bb:cc:aa:bb:cc" # netfailover devs share the same mac for iface in ["ens3", "ens3sby", "enP0s1f3"]: @@ -345,7 +346,7 @@ def is_netfail(iface, _driver=None): return False return True - self.m_netfail.side_effect = is_netfail + self.m_is_netfail.side_effect = is_netfail def is_netfail_master(iface, _driver=None): # ens3 is the master @@ -353,7 +354,7 @@ def is_netfail_master(iface, _driver=None): return True return False - self.m_netfail_master.side_effect = is_netfail_master + m_is_netfail_master.side_effect = is_netfail_master expected = { "ethernets": { "ens3": { @@ -366,23 +367,21 @@ def is_netfail_master(iface, _driver=None): "version": 2, } result = net.generate_fallback_config() - self.assertEqual(expected, result) - - -class TestNetFindFallBackNic(CiTestCase): - def setUp(self): - super(TestNetFindFallBackNic, 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) - self.add_patch( + assert expected == result + + +class TestNetFindFallBackNic: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + self.sysdir = str(tmp_path) + "/" + mocker.patch( + "cloudinit.net.get_sys_class_path", return_value=self.sysdir + ) + mocker.patch( "cloudinit.net.util.is_container", - "m_is_container", return_value=False, ) - self.add_patch("cloudinit.net.util.udevadm_settle", "m_settle") + mocker.patch("cloudinit.net.util.udevadm_settle") def test_generate_fallback_finds_first_connected_eth_with_mac(self): """find_fallback_nic finds any connected device with a mac.""" @@ -390,7 +389,7 @@ def test_generate_fallback_finds_first_connected_eth_with_mac(self): write_file(os.path.join(self.sysdir, "eth1", "carrier"), "1") mac = "aa:bb:cc:aa:bb:cc" write_file(os.path.join(self.sysdir, "eth1", "address"), mac) - self.assertEqual("eth1", net.find_fallback_nic()) + assert "eth1" == net.find_fallback_nic() class TestNetFindCandidateNics: @@ -617,66 +616,62 @@ def test_udevadm_settle_failure_handled_gracefully(self, caplog): self.m_settle.assert_called_once() -class TestGetDeviceList(CiTestCase): - def setUp(self): - super(TestGetDeviceList, 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 TestGetDeviceList: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + self.sysdir = str(tmp_path) + "/" + self.m_sys_path = mocker.patch( + "cloudinit.net.get_sys_class_path", return_value=self.sysdir + ) def test_get_devicelist_raise_oserror(self): """get_devicelist raise any non-ENOENT OSerror.""" error = OSError("Can not do it") error.errno = errno.EPERM # Set non-ENOENT self.m_sys_path.side_effect = error - with self.assertRaises(OSError) as context_manager: + with pytest.raises(OSError, match="Can not do it"): net.get_devicelist() - exception = context_manager.exception - self.assertEqual("Can not do it", str(exception)) def test_get_devicelist_empty_without_sys_net(self): """get_devicelist returns empty list when missing SYS_CLASS_NET.""" self.m_sys_path.return_value = "idontexist" - self.assertEqual([], net.get_devicelist()) + assert [] == net.get_devicelist() def test_get_devicelist_empty_with_no_devices_in_sys_net(self): """get_devicelist returns empty directoty listing for SYS_CLASS_NET.""" - self.assertEqual([], net.get_devicelist()) + assert [] == net.get_devicelist() def test_get_devicelist_lists_any_subdirectories_in_sys_net(self): """get_devicelist returns a directory listing for SYS_CLASS_NET.""" write_file(os.path.join(self.sysdir, "eth0", "operstate"), "up") write_file(os.path.join(self.sysdir, "eth1", "operstate"), "up") - self.assertCountEqual(["eth0", "eth1"], net.get_devicelist()) + assert_count_equal(["eth0", "eth1"], net.get_devicelist()) @mock.patch( "cloudinit.net.is_openvswitch_internal_interface", mock.Mock(return_value=False), ) -class TestGetInterfaceMAC(CiTestCase): - def setUp(self): - super(TestGetInterfaceMAC, 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 TestGetInterfaceMAC: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + self.sysdir = str(tmp_path) + "/" + self.m_sys_path = mocker.patch( + "cloudinit.net.get_sys_class_path", return_value=self.sysdir + ) def test_get_interface_mac_false_with_no_mac(self): """get_device_list returns False when no mac is reported.""" ensure_file(os.path.join(self.sysdir, "eth0", "bonding")) mac_path = os.path.join(self.sysdir, "eth0", "address") - self.assertFalse(os.path.exists(mac_path)) - self.assertFalse(net.get_interface_mac("eth0")) + assert not os.path.exists(mac_path) + assert net.get_interface_mac("eth0") is False def test_get_interface_mac(self): """get_interfaces returns the mac from SYS_CLASS_NET/dev/address.""" mac = "aa:bb:cc:aa:bb:cc" write_file(os.path.join(self.sysdir, "eth1", "address"), mac) - self.assertEqual(mac, net.get_interface_mac("eth1")) + assert mac == net.get_interface_mac("eth1") def test_get_interface_mac_grabs_bonding_address(self): """get_interfaces returns the source device mac for bonded devices.""" @@ -687,12 +682,12 @@ def test_get_interface_mac_grabs_bonding_address(self): os.path.join(self.sysdir, "eth1", "bonding_slave", "perm_hwaddr"), source_dev_mac, ) - self.assertEqual(source_dev_mac, net.get_interface_mac("eth1")) + assert source_dev_mac == net.get_interface_mac("eth1") def test_get_interfaces_empty_list_without_sys_net(self): """get_interfaces returns an empty list when missing SYS_CLASS_NET.""" self.m_sys_path.return_value = "idontexist" - self.assertEqual([], net.get_interfaces()) + assert [] == net.get_interfaces() def test_get_interfaces_by_mac_skips_empty_mac(self): """Ignore 00:00:00:00:00:00 addresses from get_interfaces_by_mac.""" @@ -703,18 +698,18 @@ def test_get_interfaces_by_mac_skips_empty_mac(self): write_file(os.path.join(self.sysdir, "eth2", "addr_assign_type"), "0") write_file(os.path.join(self.sysdir, "eth2", "address"), mac) expected = [("eth2", "aa:bb:cc:aa:bb:cc", None, None)] - self.assertEqual(expected, net.get_interfaces()) + assert expected == net.get_interfaces() def test_get_interfaces_by_mac_skips_missing_mac(self): """Ignore interfaces without an address from get_interfaces_by_mac.""" write_file(os.path.join(self.sysdir, "eth1", "addr_assign_type"), "0") address_path = os.path.join(self.sysdir, "eth1", "address") - self.assertFalse(os.path.exists(address_path)) + assert not os.path.exists(address_path) mac = "aa:bb:cc:aa:bb:cc" write_file(os.path.join(self.sysdir, "eth2", "addr_assign_type"), "0") write_file(os.path.join(self.sysdir, "eth2", "address"), mac) expected = [("eth2", "aa:bb:cc:aa:bb:cc", None, None)] - self.assertEqual(expected, net.get_interfaces()) + assert expected == net.get_interfaces() def test_get_interfaces_by_mac_skips_master_devs(self): """Ignore interfaces with a master device which would have dup mac.""" @@ -725,7 +720,7 @@ def test_get_interfaces_by_mac_skips_master_devs(self): write_file(os.path.join(self.sysdir, "eth2", "addr_assign_type"), "0") write_file(os.path.join(self.sysdir, "eth2", "address"), mac2) expected = [("eth2", mac2, None, None)] - self.assertEqual(expected, net.get_interfaces()) + assert expected == net.get_interfaces() @mock.patch("cloudinit.net.is_netfailover") def test_get_interfaces_by_mac_skips_netfailvoer(self, m_netfail): @@ -746,7 +741,7 @@ def is_netfail(iface, _driver=None): m_netfail.side_effect = is_netfail expected = [("ens3", mac, None, None)] - self.assertEqual(expected, net.get_interfaces()) + assert expected == net.get_interfaces() def test_get_interfaces_does_not_skip_phys_members_of_bridges_and_bonds( self, @@ -779,24 +774,23 @@ def test_get_interfaces_does_not_skip_phys_members_of_bridges_and_bonds( ) interface_names = [interface[0] for interface in net.get_interfaces()] - self.assertEqual( - ["eth1", "eth2", "eth3", "ovs-system"], sorted(interface_names) + assert ["eth1", "eth2", "eth3", "ovs-system"] == sorted( + interface_names ) -class TestInterfaceHasOwnMAC(CiTestCase): - def setUp(self): - super(TestInterfaceHasOwnMAC, 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 TestInterfaceHasOwnMAC: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + self.sysdir = str(tmp_path) + "/" + mocker.patch( + "cloudinit.net.get_sys_class_path", return_value=self.sysdir + ) def test_interface_has_own_mac_false_when_stolen(self): """Return False from interface_has_own_mac when address is stolen.""" write_file(os.path.join(self.sysdir, "eth1", "addr_assign_type"), "2") - self.assertFalse(net.interface_has_own_mac("eth1")) + assert net.interface_has_own_mac("eth1") is False def test_interface_has_own_mac_true_when_not_stolen(self): """Return False from interface_has_own_mac when mac isn't stolen.""" @@ -804,27 +798,23 @@ def test_interface_has_own_mac_true_when_not_stolen(self): assign_path = os.path.join(self.sysdir, "eth1", "addr_assign_type") for _type in valid_assign_types: write_file(assign_path, _type) - self.assertTrue(net.interface_has_own_mac("eth1")) + assert net.interface_has_own_mac("eth1") is True def test_interface_has_own_mac_strict_errors_on_absent_assign_type(self): """When addr_assign_type is absent, interface_has_own_mac errors.""" - with self.assertRaises(ValueError): + with pytest.raises(ValueError): net.interface_has_own_mac("eth1", strict=True) @mock.patch("cloudinit.net.subp.subp") @pytest.mark.usefixtures("disable_netdev_info") -class TestEphemeralIPV4Network(CiTestCase): - - with_logs = True - - def setUp(self): - super(TestEphemeralIPV4Network, 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 TestEphemeralIPV4Network: + @pytest.fixture(autouse=True) + def fixtures(self, mocker, tmp_path): + self.sysdir = str(tmp_path) + "/" + mocker.patch( + "cloudinit.net.get_sys_class_path", return_value=self.sysdir + ) def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp): """No required params for EphemeralIPv4Network can be None.""" @@ -837,15 +827,13 @@ def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp): for key in required_params.keys(): params = copy.deepcopy(required_params) params[key] = None - with self.assertRaises(ValueError) as context_manager: + with pytest.raises(ValueError, match="Cannot init network on"): EphemeralIPv4Network( MockDistro(), interface_addrs_before_dhcp=example_netdev, **params, ) - error = context_manager.exception - self.assertIn("Cannot init network on", str(error)) - self.assertEqual(0, m_subp.call_count) + assert 0 == m_subp.call_count def test_ephemeral_ipv4_network_errors_invalid_mask_prefix(self, m_subp): """Raise an error when prefix_or_mask is not a netmask or prefix.""" @@ -858,14 +846,13 @@ def test_ephemeral_ipv4_network_errors_invalid_mask_prefix(self, m_subp): invalid_masks = ("invalid", "invalid.", "123.123.123") for error_val in invalid_masks: params["prefix_or_mask"] = error_val - with self.assertRaises(ValueError) as context_manager: + with pytest.raises( + ValueError, + match="Cannot setup network, invalid prefix or netmask: ", + ): with EphemeralIPv4Network(MockDistro(), **params): pass - error = context_manager.exception - self.assertIn( - "Cannot setup network, invalid prefix or netmask: ", str(error) - ) - self.assertEqual(0, m_subp.call_count) + assert 0 == m_subp.call_count def test_ephemeral_ipv4_network_performs_teardown(self, m_subp): """EphemeralIPv4Network performs teardown on the device if setup.""" @@ -907,7 +894,7 @@ def test_ephemeral_ipv4_network_performs_teardown(self, m_subp): "interface_addrs_before_dhcp": example_netdev, } with EphemeralIPv4Network(MockDistro(), **params): - self.assertEqual(expected_setup_calls, m_subp.call_args_list) + assert expected_setup_calls == m_subp.call_args_list m_subp.assert_has_calls(expected_teardown_calls) def test_teardown_on_enter_exception(self, m_subp): @@ -969,7 +956,7 @@ def side_effect(args, **kwargs): for teardown in expected_teardown_calls: assert teardown in m_subp.call_args_list - def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp): + def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp, caplog): """EphemeralIPv4Network handles exception when address is setup. It performs no cleanup as the interface was already setup. @@ -988,8 +975,8 @@ def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp): with EphemeralIPv4Network(MockDistro(), **params): pass assert expected_calls == m_subp.call_args_list - assert "Skip bringing up network link" in self.logs.getvalue() - assert "Skip adding ip address" in self.logs.getvalue() + assert "Skip bringing up network link" in caplog.text + assert "Skip adding ip address" in caplog.text def test_ephemeral_ipv4_network_with_prefix(self, m_subp): """EphemeralIPv4Network takes a valid prefix to setup the network.""" @@ -1116,7 +1103,7 @@ def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp): ] with EphemeralIPv4Network(MockDistro(), **params): - self.assertEqual(expected_setup_calls, m_subp.call_args_list) + assert expected_setup_calls == m_subp.call_args_list m_subp.assert_has_calls(expected_teardown_calls) def test_ephemeral_ipv4_network_with_rfc3442_static_routes(self, m_subp): @@ -1230,7 +1217,7 @@ def test_ephemeral_ipv4_network_with_rfc3442_static_routes(self, m_subp): ), ] with EphemeralIPv4Network(MockDistro(), **params): - self.assertEqual(expected_setup_calls, m_subp.call_args_list) + assert expected_setup_calls == m_subp.call_args_list m_subp.assert_has_calls(expected_setup_calls + expected_teardown_calls) @@ -1272,11 +1259,13 @@ def _mk_v2_phys(mac, name, driver=None, device_id=None): return v2_cfg -class TestExtractPhysdevs(CiTestCase): - def setUp(self): - super(TestExtractPhysdevs, self).setUp() - self.add_patch("cloudinit.net.device_driver", "m_driver") - self.add_patch("cloudinit.net.device_devid", "m_devid") +class TestExtractPhysdevs: + @pytest.fixture(autouse=True) + def fixtures(self, mocker): + self.m_driver = mocker.patch("cloudinit.net.device_driver") + self.m_devid = mocker.patch( + "cloudinit.net.device_devid", + ) def test_extract_physdevs_looks_up_driver_v1(self): driver = "virtio" @@ -1290,9 +1279,7 @@ def test_extract_physdevs_looks_up_driver_v1(self): } # insert the driver value for verification physdevs[0][2] = driver - self.assertEqual( - sorted(physdevs), sorted(net.extract_physdevs(netcfg)) - ) + assert sorted(physdevs) == sorted(net.extract_physdevs(netcfg)) self.m_driver.assert_called_with("eth0") def test_extract_physdevs_looks_up_driver_v2(self): @@ -1307,9 +1294,7 @@ def test_extract_physdevs_looks_up_driver_v2(self): } # insert the driver value for verification physdevs[0][2] = driver - self.assertEqual( - sorted(physdevs), sorted(net.extract_physdevs(netcfg)) - ) + assert sorted(physdevs) == sorted(net.extract_physdevs(netcfg)) self.m_driver.assert_called_with("eth0") def test_extract_physdevs_looks_up_devid_v1(self): @@ -1324,9 +1309,7 @@ def test_extract_physdevs_looks_up_devid_v1(self): } # insert the driver value for verification physdevs[0][3] = devid - self.assertEqual( - sorted(physdevs), sorted(net.extract_physdevs(netcfg)) - ) + assert sorted(physdevs) == sorted(net.extract_physdevs(netcfg)) self.m_devid.assert_called_with("eth0") def test_extract_physdevs_looks_up_devid_v2(self): @@ -1341,9 +1324,7 @@ def test_extract_physdevs_looks_up_devid_v2(self): } # insert the driver value for verification physdevs[0][3] = devid - self.assertEqual( - sorted(physdevs), sorted(net.extract_physdevs(netcfg)) - ) + assert sorted(physdevs) == sorted(net.extract_physdevs(netcfg)) self.m_devid.assert_called_with("eth0") def test_get_v1_type_physical(self): @@ -1356,9 +1337,7 @@ def test_get_v1_type_physical(self): "version": 1, "config": [_mk_v1_phys(*args) for args in physdevs], } - self.assertEqual( - sorted(physdevs), sorted(net.extract_physdevs(netcfg)) - ) + assert sorted(physdevs) == sorted(net.extract_physdevs(netcfg)) def test_get_v2_type_physical(self): physdevs = [ @@ -1370,9 +1349,7 @@ def test_get_v2_type_physical(self): "version": 2, "ethernets": {args[1]: _mk_v2_phys(*args) for args in physdevs}, } - self.assertEqual( - sorted(physdevs), sorted(net.extract_physdevs(netcfg)) - ) + assert sorted(physdevs) == sorted(net.extract_physdevs(netcfg)) def test_get_v2_type_physical_skips_if_no_set_name(self): netcfg = { @@ -1383,10 +1360,10 @@ def test_get_v2_type_physical_skips_if_no_set_name(self): } }, } - self.assertEqual([], net.extract_physdevs(netcfg)) + assert [] == net.extract_physdevs(netcfg) def test_runtime_error_on_unknown_netcfg_version(self): - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): net.extract_physdevs({"version": 3, "awesome_config": []}) From 8ebe74fd57e6d5c42f2f294db46ed6cd3b2a4a28 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Tue, 26 Aug 2025 12:29:55 +0200 Subject: [PATCH 103/150] test: drop citestcase from test_reporting_hyperv.py (#6413) --- .../reporting/test_reporting_hyperv.py | 127 +++++++++--------- 1 file changed, 61 insertions(+), 66 deletions(-) diff --git a/tests/unittests/reporting/test_reporting_hyperv.py b/tests/unittests/reporting/test_reporting_hyperv.py index 31b0408d1b1..c346ac5b4cc 100644 --- a/tests/unittests/reporting/test_reporting_hyperv.py +++ b/tests/unittests/reporting/test_reporting_hyperv.py @@ -8,7 +8,8 @@ import zlib from unittest import mock -from cloudinit import util +import pytest + from cloudinit.reporting import events, instantiated_handler_registry from cloudinit.reporting.handlers import HyperVKvpReportingHandler @@ -17,28 +18,33 @@ # long term we should restructure these modules to avoid the issue. from cloudinit.sources.azure import errors # noqa: F401 from cloudinit.sources.helpers import azure -from tests.unittests.helpers import CiTestCase -class TestKvpEncoding(CiTestCase): +class TestKvpEncoding: def test_encode_decode(self): kvp = {"key": "key1", "value": "value1"} kvp_reporting = HyperVKvpReportingHandler() data = kvp_reporting._encode_kvp_item(kvp["key"], kvp["value"]) - self.assertEqual(len(data), kvp_reporting.HV_KVP_RECORD_SIZE) + assert len(data) == kvp_reporting.HV_KVP_RECORD_SIZE decoded_kvp = kvp_reporting._decode_kvp_item(data) - self.assertEqual(kvp, decoded_kvp) + assert kvp == decoded_kvp + +class TestKvpReporter: + @pytest.fixture + def kvp_file_path(self, tmp_path): + file_path = tmp_path / "kvp_pool_file" + file_path.touch() + return str(file_path) -class TextKvpReporter(CiTestCase): - def setUp(self): - super(TextKvpReporter, self).setUp() - self.tmp_file_path = self.tmp_path("kvp_pool_file") - util.ensure_file(self.tmp_file_path) + @pytest.fixture + def reporter(self, kvp_file_path): + return HyperVKvpReportingHandler(kvp_file_path=kvp_file_path) - def test_events_with_higher_incarnation_not_over_written(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) - self.assertEqual(0, len(list(reporter._iterate_kvps(0)))) + def test_events_with_higher_incarnation_not_over_written( + self, kvp_file_path, reporter + ): + assert 0 == len(list(reporter._iterate_kvps(0))) reporter.publish_event( events.ReportingEvent("foo", "name1", "description") @@ -47,29 +53,27 @@ def test_events_with_higher_incarnation_not_over_written(self): events.ReportingEvent("foo", "name2", "description") ) reporter.q.join() - self.assertEqual(2, len(list(reporter._iterate_kvps(0)))) + assert 2 == len(list(reporter._iterate_kvps(0))) - reporter3 = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + reporter3 = HyperVKvpReportingHandler(kvp_file_path=kvp_file_path) reporter3.incarnation_no = reporter.incarnation_no - 1 reporter3.publish_event( events.ReportingEvent("foo", "name3", "description") ) reporter3.q.join() - self.assertEqual(3, len(list(reporter3._iterate_kvps(0)))) + assert 3 == len(list(reporter3._iterate_kvps(0))) - def test_finish_event_result_is_logged(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_finish_event_result_is_logged(self, reporter): reporter.publish_event( events.FinishReportingEvent( "name2", "description1", result=events.status.FAIL ) ) reporter.q.join() - self.assertIn("FAIL", list(reporter._iterate_kvps(0))[0]["value"]) + assert "FAIL" in list(reporter._iterate_kvps(0))[0]["value"] - def test_file_operation_issue(self): - os.remove(self.tmp_file_path) - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_file_operation_issue(self, kvp_file_path, reporter): + os.remove(kvp_file_path) reporter.publish_event( events.FinishReportingEvent( "name2", "description1", result=events.status.FAIL @@ -77,8 +81,7 @@ def test_file_operation_issue(self): ) reporter.q.join() - def test_event_very_long(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_event_very_long(self, reporter): description = "ab" * reporter.HV_KVP_AZURE_MAX_VALUE_SIZE long_event = events.FinishReportingEvent( "event_name", description, result=events.status.FAIL @@ -86,18 +89,20 @@ def test_event_very_long(self): reporter.publish_event(long_event) reporter.q.join() kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(3, len(kvps)) + assert 3 == len(kvps) # restore from the kvp to see the content are all there full_description = "" for i in range(len(kvps)): msg_slice = json.loads(kvps[i]["value"]) - self.assertEqual(msg_slice["msg_i"], i) + assert msg_slice["msg_i"] == i full_description += msg_slice["msg"] - self.assertEqual(description, full_description) + assert description == full_description - def test_not_truncate_kvp_file_modified_after_boot(self): - with open(self.tmp_file_path, "wb+") as f: + def test_not_truncate_kvp_file_modified_after_boot( + self, kvp_file_path, reporter + ): + with open(kvp_file_path, "wb+") as f: kvp = {"key": "key1", "value": "value1"} data = struct.pack( "%ds%ds" @@ -110,18 +115,17 @@ def test_not_truncate_kvp_file_modified_after_boot(self): ) f.write(data) cur_time = time.time() - os.utime(self.tmp_file_path, (cur_time, cur_time)) + os.utime(kvp_file_path, (cur_time, cur_time)) # reset this because the unit test framework # has already polluted the class variable HyperVKvpReportingHandler._already_truncated_pool_file = False - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(1, len(kvps)) + assert 1 == len(kvps) - def test_truncate_stale_kvp_file(self): - with open(self.tmp_file_path, "wb+") as f: + def test_truncate_stale_kvp_file(self, kvp_file_path, reporter): + with open(kvp_file_path, "wb+") as f: kvp = {"key": "key1", "value": "value1"} data = struct.pack( "%ds%ds" @@ -136,20 +140,19 @@ def test_truncate_stale_kvp_file(self): # set the time ways back to make it look like # we had an old kvp file - os.utime(self.tmp_file_path, (1000000, 1000000)) + os.utime(kvp_file_path, (1000000, 1000000)) # reset this because the unit test framework # has already polluted the class variable HyperVKvpReportingHandler._already_truncated_pool_file = False - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + reporter = HyperVKvpReportingHandler(kvp_file_path=kvp_file_path) kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(0, len(kvps)) + assert 0 == len(kvps) @mock.patch("cloudinit.distros.uses_systemd") @mock.patch("cloudinit.subp.subp") - def test_get_boot_telemetry(self, m_subp, m_sysd): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_get_boot_telemetry(self, m_subp, m_sysd, reporter): datetime_pattern = ( r"\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]" r"\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)" @@ -166,7 +169,7 @@ def test_get_boot_telemetry(self, m_subp, m_sysd): reporter.publish_event(azure.get_boot_telemetry()) reporter.q.join() kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(1, len(kvps)) + assert 1 == len(kvps) evt_msg = kvps[0]["value"] if not re.search("kernel_start=" + datetime_pattern, evt_msg): @@ -176,14 +179,13 @@ def test_get_boot_telemetry(self, m_subp, m_sysd): if not re.search("cloudinit_activation=" + datetime_pattern, evt_msg): raise AssertionError("missing cloudinit_activation timestamp") - def test_get_system_info(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_get_system_info(self, reporter): pattern = r"[^=\s]+" reporter.publish_event(azure.get_system_info()) reporter.q.join() kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(1, len(kvps)) + assert 1 == len(kvps) evt_msg = kvps[0]["value"] # the most important information is cloudinit version, @@ -196,20 +198,18 @@ def test_get_system_info(self): if not re.search("variant=" + pattern, evt_msg): raise AssertionError("missing distro variant string") - def test_report_diagnostic_event_without_logger_func(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_report_diagnostic_event_without_logger_func(self, reporter): diagnostic_msg = "test_diagnostic" reporter.publish_event(azure.report_diagnostic_event(diagnostic_msg)) reporter.q.join() kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(1, len(kvps)) + assert 1 == len(kvps) evt_msg = kvps[0]["value"] if diagnostic_msg not in evt_msg: raise AssertionError("missing expected diagnostic message") - def test_report_diagnostic_event_with_logger_func(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_report_diagnostic_event_with_logger_func(self, reporter): logger_func = mock.MagicMock() diagnostic_msg = "test_diagnostic" reporter.publish_event( @@ -219,15 +219,14 @@ def test_report_diagnostic_event_with_logger_func(self): ) reporter.q.join() kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(1, len(kvps)) + assert 1 == len(kvps) evt_msg = kvps[0]["value"] if diagnostic_msg not in evt_msg: raise AssertionError("missing expected diagnostic message") logger_func.assert_called_once_with(diagnostic_msg) - def test_report_compressed_event(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_report_compressed_event(self, reporter): try: instantiated_handler_registry.register_item("telemetry", reporter) event_desc = b"test_compressed" @@ -257,14 +256,13 @@ def validate_compressed_kvps(self, reporter, count, values): base64.decodebytes(evt_msg_json["data"].encode("ascii")) ) - self.assertLess(compressed_count, len(values)) - self.assertEqual(evt_data, values[compressed_count]) - self.assertEqual(evt_encoding, "gz+b64") + assert compressed_count < len(values) + assert evt_data == values[compressed_count] + assert evt_encoding == "gz+b64" compressed_count += 1 - self.assertEqual(compressed_count, count) + assert compressed_count == count - def test_unique_kvp_key(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_unique_kvp_key(self, reporter): evt1 = events.ReportingEvent( "event_type", "event_message", "event_description" ) @@ -280,21 +278,18 @@ def test_unique_kvp_key(self): reporter.q.join() kvps = list(reporter._iterate_kvps(0)) - self.assertEqual(2, len(kvps)) - self.assertNotEqual( - kvps[0]["key"], kvps[1]["key"], "duplicate keys for KVP entries" - ) + assert 2 == len(kvps) + assert ( + kvps[0]["key"] != kvps[1]["key"] + ), "duplicate keys for KVP entries" - def test_write_key(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) + def test_write_key(self, reporter): reporter.write_key("test-key", "test-value") assert list(reporter._iterate_kvps(0)) == [ {"key": "test-key", "value": "test-value"} ] - def test_write_key_truncates(self): - reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path) - + def test_write_key_truncates(self, reporter): value = "A" * 2000 reporter.write_key("test-key", value) From 16f2039d0705ee9873ace98c967a34e6da6d0b87 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 27 Aug 2025 09:24:10 +0200 Subject: [PATCH 104/150] test: remove CiTestCase fully (#6416) --- conftest.py | 9 +-- tests/unittests/helpers.py | 110 +-------------------------- tests/unittests/sources/test_init.py | 6 +- tests/unittests/test_conftest.py | 25 ------ 4 files changed, 6 insertions(+), 144 deletions(-) diff --git a/conftest.py b/conftest.py index 5d7aa5638d1..52d8d5466da 100644 --- a/conftest.py +++ b/conftest.py @@ -95,8 +95,8 @@ def disable_subp_usage(request, fixture_utils): Note that this can only catch invocations where the ``subp`` module is imported and ``subp.subp(...)`` is called. ``from cloudinit.subp import - subp`` imports happen before the patching here (or the CiTestCase - monkey-patching) happens, so are left untouched. + subp`` is left untouched because those imports happen before the patching + happens here. While ``disable_subp_usage`` unconditionally patches ``cloudinit.subp.subp``, any test-local patching will override this @@ -128,11 +128,6 @@ def test_bash(self): def test_several_things(self): subp.subp(["bash"]) subp.subp(["whoami"]) - - This fixture (roughly) mirrors the functionality of - ``CiTestCase.allowed_subp``. N.B. While autouse fixtures do affect - non-pytest tests, CiTestCase's ``allowed_subp`` does take precedence (and - we have ``TestDisableSubpUsageInTestSubclass`` to confirm that). """ allow_subp_for = fixture_utils.closest_marker_args_or( request, "allow_subp_for", None diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 7e9eda8c1b8..b107a75bce3 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -2,25 +2,20 @@ # pylint: disable=attribute-defined-outside-init import copy -import functools -import io -import logging import os import random import shutil import string -import tempfile import time import unittest from contextlib import contextmanager -from typing import ClassVar, List, Union from unittest import mock from unittest.util import strclass from urllib.parse import urlsplit, urlunsplit import responses -from cloudinit import distros, helpers, settings, subp, util +from cloudinit import distros, helpers, settings, util from cloudinit.config.schema import ( SchemaValidationError, validate_cloudconfig_schema, @@ -30,8 +25,6 @@ from tests.helpers import cloud_init_project_dir from tests.hypothesis_jsonschema import HAS_HYPOTHESIS_JSONSCHEMA -_real_subp = subp.subp - # Used for skipping tests SkipTest = unittest.SkipTest skipIf = unittest.skipIf @@ -164,107 +157,6 @@ def add_patch(self, target, attr, *args, **kwargs): setattr(self, attr, p) -class CiTestCase(TestCase): - """This is the preferred test case base class unless user - needs other test case classes below.""" - - # Subclass overrides for specific test behavior - # Whether or not a unit test needs logfile setup - with_logs = False - allowed_subp: ClassVar[Union[List, bool]] = False - SUBP_SHELL_TRUE = "shell=true" - - @contextmanager - def allow_subp(self, allowed_subp): - orig = self.allowed_subp - try: - self.allowed_subp = allowed_subp - yield - finally: - self.allowed_subp = orig - - def setUp(self): - super(CiTestCase, self).setUp() - if self.with_logs: - # Create a log handler so unit tests can search expected logs. - self.logger = logging.getLogger() - self.logs = io.StringIO() - formatter = logging.Formatter("%(levelname)s: %(message)s") - handler = logging.StreamHandler(self.logs) - handler.setFormatter(formatter) - self.old_handlers = self.logger.handlers - self.logger.handlers = [handler] - self.old_level = logging.root.level - self.logger.level = logging.DEBUG - if self.allowed_subp is True: - subp.subp = _real_subp - else: - subp.subp = self._fake_subp - - def _fake_subp(self, *args, **kwargs): - if "args" in kwargs: - cmd = kwargs["args"] - else: - if not args: - raise TypeError( - "subp() missing 1 required positional argument: 'args'" - ) - cmd = args[0] - - if not isinstance(cmd, str): - cmd = cmd[0] - pass_through = False - if not isinstance(self.allowed_subp, (list, bool)): - raise TypeError("self.allowed_subp supports list or bool.") - if isinstance(self.allowed_subp, bool): - pass_through = self.allowed_subp - else: - pass_through = (cmd in self.allowed_subp) or ( - self.SUBP_SHELL_TRUE in self.allowed_subp - and kwargs.get("shell") - ) - if pass_through: - return _real_subp(*args, **kwargs) - raise RuntimeError( - "called subp. set self.allowed_subp=True to allow\n subp(%s)" - % ", ".join( - [str(repr(a)) for a in args] - + ["%s=%s" % (k, repr(v)) for k, v in kwargs.items()] - ) - ) - - def tearDown(self): - if self.with_logs: - # Remove the handler we setup - logging.getLogger().handlers = self.old_handlers - logging.getLogger().setLevel(self.old_level) - subp.subp = _real_subp - super(CiTestCase, self).tearDown() - - def tmp_dir(self, dir=None, cleanup=True): - # return a full path to a temporary directory that will be cleaned up. - if dir is None: - tmpd = tempfile.mkdtemp(prefix="ci-%s." % self.__class__.__name__) - else: - tmpd = tempfile.mkdtemp(dir=dir) - self.addCleanup( - functools.partial(shutil.rmtree, tmpd, ignore_errors=True) - ) - return tmpd - - def tmp_path(self, path, dir=None): - # return an absolute path to 'path' under dir. - # if dir is None, one will be created with tmp_dir() - # the file is not created or modified. - if dir is None: - dir = self.tmp_dir() - return os.path.normpath(os.path.abspath(os.path.join(dir, path))) - - @classmethod - def random_string(cls, length=8): - return random_string(length) - - def replicate_test_root(example_root, target_root): real_root = resourceLocation() real_root = os.path.join(real_root, "roots", example_root) diff --git a/tests/unittests/sources/test_init.py b/tests/unittests/sources/test_init.py index 05bebd6e0d9..f4a6cc6b1e7 100644 --- a/tests/unittests/sources/test_init.py +++ b/tests/unittests/sources/test_init.py @@ -24,7 +24,7 @@ redact_sensitive_keys, ) from cloudinit.user_data import UserDataProcessor -from tests.unittests.helpers import CiTestCase, assert_count_equal, mock +from tests.unittests.helpers import assert_count_equal, mock class DataSourceTestSubclassNet(DataSource): @@ -933,7 +933,7 @@ def fake_get_data(): ) in caplog.record_tuples -class TestRedactSensitiveData(CiTestCase): +class TestRedactSensitiveData: def test_redact_sensitive_data_noop_when_no_sensitive_keys_present(self): """When sensitive_keys is absent or empty from metadata do nothing.""" md = {"my": "data"} @@ -962,7 +962,7 @@ def test_redact_sensitive_data_does_redacts_with_default_string(self): assert secure_md == redact_sensitive_keys(md) -class TestCanonicalCloudID(CiTestCase): +class TestCanonicalCloudID: def test_cloud_id_returns_platform_on_unknowns(self): """When region and cloud_name are unknown, return platform.""" assert "platform" == canonical_cloud_id( diff --git a/tests/unittests/test_conftest.py b/tests/unittests/test_conftest.py index d1a4be23f1c..bc2db355f16 100644 --- a/tests/unittests/test_conftest.py +++ b/tests/unittests/test_conftest.py @@ -2,7 +2,6 @@ from cloudinit import subp from conftest import UnexpectedSubpError -from tests.unittests.helpers import CiTestCase class TestDisableSubpUsage: @@ -51,27 +50,3 @@ def test_subp_usage_can_be_conditionally_reenabled_for_multiple_cmds(self): def test_both_marks_raise_an_error(self): with pytest.raises(UnexpectedSubpError, match="marked both"): subp.subp(["sh"]) - - -class TestDisableSubpUsageInTestSubclass(CiTestCase): - """Test that disable_subp_usage doesn't impact CiTestCase's subp logic. - - Once the rest of the CiTestCase tests are removed, this class - should be removed as well. - """ - - def test_using_subp_raises_exception(self): - with pytest.raises(Exception): - subp.subp(["some", "args"]) - - def test_typeerrors_on_incorrect_usage(self): - with pytest.raises(TypeError): - subp.subp() - - def test_subp_usage_can_be_reenabled(self): - _old_allowed_subp = self.allow_subp - self.allowed_subp = True - try: - subp.subp(["sh", "-c", "true"]) - finally: - self.allowed_subp = _old_allowed_subp From 6645635047df3601c641a388c64683d9440f6efa Mon Sep 17 00:00:00 2001 From: abdulganiyy Date: Wed, 27 Aug 2025 22:47:15 +0100 Subject: [PATCH 105/150] fixed defs types in cloud-init cmd devel make_mime (#6419) --- cloudinit/cmd/devel/make_mime.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cloudinit/cmd/devel/make_mime.py b/cloudinit/cmd/devel/make_mime.py index 8b2c68ccd49..5411ad602d1 100755 --- a/cloudinit/cmd/devel/make_mime.py +++ b/cloudinit/cmd/devel/make_mime.py @@ -9,6 +9,7 @@ import sys from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from typing import List, Optional from cloudinit.handlers import INCLUSION_TYPES_MAP @@ -22,7 +23,7 @@ def create_mime_message(files): sub_messages = [] - errors = [] + errors: List[str] = [] for i, (fh, filename, format_type) in enumerate(files): contents = fh.read() sub_message = MIMEText(contents, format_type, sys.getdefaultencoding()) @@ -43,18 +44,20 @@ def create_mime_message(files): return (combined_message, errors) -def file_content_type(text): +def file_content_type(text: str): """Return file content type by reading the first line of the input.""" try: filename, content_type = text.split(":", 1) return (open(filename, "r"), filename, content_type.strip()) except ValueError as e: raise argparse.ArgumentError( - text, "Invalid value for %r" % (text) + None, "Invalid value for %r" % (text) ) from e -def get_parser(parser=None): +def get_parser( + parser: Optional[argparse.ArgumentParser] = None, +) -> argparse.ArgumentParser: """Build or extend and arg parser for make-mime utility. @param parser: Optional existing ArgumentParser instance representing the @@ -94,7 +97,7 @@ def get_parser(parser=None): return parser -def get_content_types(strip_prefix=False): +def get_content_types(strip_prefix: bool = False) -> List[str]: """Return a list of cloud-init supported content types. Optionally strip out the leading 'text/' of the type if strip_prefix=True. """ @@ -106,7 +109,7 @@ def get_content_types(strip_prefix=False): ) -def handle_args(name, args): +def handle_args(name: str, args: argparse.Namespace) -> int: """Create a multi-part MIME archive for use as user-data. Optionally print out the list of supported content types of cloud-init. From 93af2dda8447eda9bc32940722cf878544f1776e Mon Sep 17 00:00:00 2001 From: Paul <44974737+paulober@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:51:05 +0100 Subject: [PATCH 106/150] fix(raspberry-pi-os): Update ntp client (#6425) Updates the ntp client used on Raspberry Pi OS to `systemd-timesyncd`. Signed-off-by: paulober --- cloudinit/config/cc_ntp.py | 4 ++-- config/cloud.cfg.tmpl | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index a8f1defbfe3..adc17bf0aae 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -213,8 +213,8 @@ }, }, "raspberry-pi-os": { - "chrony": { - "confpath": "/etc/chrony/chrony.conf", + "systemd-timesyncd": { + "check_exe": "/usr/lib/systemd/systemd-timesyncd", }, }, "rhel": { diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index dea7f0312a1..96fe02ede80 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -336,8 +336,6 @@ system_info: {% if variant in ["debian", "ubuntu", "unknown"] %} # Automatically discover the best ntp_client ntp_client: auto -{% elif variant == "raspberry-pi-os" %} - ntp_client: 'systemd-timesyncd' {% endif %} {% if variant in ["alpine", "amazon", "aosc", "arch", "azurelinux", "debian", "fedora", "gentoo", "mariner", "OpenCloudOS", "openeuler", From 8b05eaa0d9db1f20d15900f95b5d465efb6f3c08 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 27 Aug 2025 23:55:24 +0200 Subject: [PATCH 107/150] Refresh dev docs for tests (#6428) Promote the use of pytest and discourage the use of TestCase tests. Include relevant fixtures and update recommendations. --- doc/rtd/development/testing.rst | 60 ++++++++++++++++++++------------- tests/unittests/helpers.py | 23 ++----------- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/doc/rtd/development/testing.rst b/doc/rtd/development/testing.rst index a94641a7447..1ac90c8aef1 100644 --- a/doc/rtd/development/testing.rst +++ b/doc/rtd/development/testing.rst @@ -9,8 +9,12 @@ be found at :file:`tests/unittests`. Integration tests can be found at tests can be found on the :ref:`integration_tests` page, but the guidelines specified below apply to both types of tests. -``Cloud-init`` uses `pytest`_ to run its tests, and has tests written both -as ``unittest.TestCase`` sub-classes and as un-subclassed ``pytest`` tests. +``Cloud-init`` uses `pytest`_ to write and run its tests. + +.. note:: + While there are a subset of tests written as ``unittest.TestCase`` + sub-classes, this is due to historical reasons. Their use is discouraged and + they are tracked to be removed in `#6427`_. Guidelines ========== @@ -20,36 +24,40 @@ The following guidelines should be followed. Test layout ----------- -* For ease of organisation and greater accessibility for developers unfamiliar - with ``pytest``, all ``cloud-init`` unit tests must be contained within test - classes. In other words, module-level test functions should not be used. +* For consistency, unit test files should have a matching name and + directory location under :file:`tests/unittests`. + +* E.g., the expected test file for code in :file:`cloudinit/path/to/file.py` + is :file:`tests/unittests/path/to/test_file.py`. + +``pytest`` guidelines +--------------------- + +* Use `pytest fixtures`_ to share functionality instead of inheritance. -* Since all tests are contained within classes, it is acceptable to mix - ``TestCase`` test classes and ``pytest`` test classes within the same - test file. +* Use bare ``assert`` statements, to take advantage of ``pytest``'s + `assertion introspection`_. - * These can be easily distinguished by their definition: ``pytest`` - classes will not use inheritance at all (e.g., - `TestGetPackageMirrorInfo`_), whereas ``TestCase`` classes will - subclass (indirectly) from ``TestCase`` (e.g., - `TestPrependBaseCommands`_). +* Prefer ``pytest``'s + `parametrized tests `__ + over test repetition. -* Unit tests and integration tests are located under :file:`cloud-init/tests`. +In-house fixtures +----------------- - * For consistency, unit test files should have a matching name and - directory location under :file:`tests/unittests`. +Before implementing your own fixture do search in :file:`*/conftest.py` files +as it could be already implemented. Another source to look for test helpers is +:file:`tests/*/helpers.py`. - * E.g., the expected test file for code in :file:`cloudinit/path/to/file.py` - is :file:`tests/unittests/path/to/test_file.py`. +Relevant fixtures: -``pytest`` tests ----------------- +* `disable_subp_usage`_ auto-disables call to subprocesses. See its + documentation to disable it. -* ``pytest`` test classes should use `pytest fixtures`_ to share - functionality instead of inheritance. +* `fake_filesystem`_ makes tests run on a temporary filesystem. -* ``pytest`` tests should use bare ``assert`` statements, to take advantage - of ``pytest``'s `assertion introspection`_. +* `paths`_ provides an instance of `cloudinit.helper.Paths` pointing to a + temporary filesystem. Dependency versions ------------------- @@ -134,3 +142,7 @@ Test argument ordering .. _pytest 3.0: https://docs.pytest.org/en/latest/changelog.html#id1093 .. _pytest.param: https://docs.pytest.org/en/6.2.x/reference.html#pytest-param .. _autospecced: https://docs.python.org/3.8/library/unittest.mock.html#autospeccing +.. _#6427: https://github.com/canonical/cloud-init/issues/6427 +.. _disable_subp_usage: https://github.com/canonical/cloud-init/blob/16f2039d0705ee9873ace98c967a34e6da6d0b87/conftest.py#L92 +.. _fake_filesystem: https://github.com/canonical/cloud-init/blob/16f2039d0705ee9873ace98c967a34e6da6d0b87/tests/unittests/conftest.py#L114 +.. _paths: https://github.com/canonical/cloud-init/blob/16f2039d0705ee9873ace98c967a34e6da6d0b87/tests/unittests/conftest.py#L224 diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index b107a75bce3..a52860b49f7 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -16,17 +16,12 @@ import responses from cloudinit import distros, helpers, settings, util -from cloudinit.config.schema import ( - SchemaValidationError, - validate_cloudconfig_schema, -) from cloudinit.helpers import Paths from cloudinit.templater import JINJA_AVAILABLE from tests.helpers import cloud_init_project_dir from tests.hypothesis_jsonschema import HAS_HYPOTHESIS_JSONSCHEMA # Used for skipping tests -SkipTest = unittest.SkipTest skipIf = unittest.skipIf @@ -123,6 +118,8 @@ def random_string(length=8): ) +# Note: The use of this class and unittests.TestCase is discouraged. Use pytest +# instead. See development docs on testing. class TestCase(unittest.TestCase): def reset_global_state(self): """Reset any global state to its original settings. @@ -220,22 +217,6 @@ def __init__(self, path_cfgs: dict, ds=None): return MockPaths -class SchemaTestCaseMixin(unittest.TestCase): - def assertSchemaValid(self, cfg, msg="Valid Schema failed validation."): - """Assert the config is valid per self.schema. - - If there is only one top level key in the schema properties, then - the cfg will be put under that key.""" - props = list(self.schema.get("properties")) - # put cfg under top level key if there is only one in the schema - if len(props) == 1: - cfg = {props[0]: cfg} - try: - validate_cloudconfig_schema(cfg, self.schema, strict=True) - except SchemaValidationError: - self.fail(msg) - - def populate_dir(path, files): if not os.path.exists(path): os.makedirs(path) From 6de2a6e731b83e157cbcd9d77d10bce3f6af1c1d Mon Sep 17 00:00:00 2001 From: Vladimir Levin <34550675+opa-oz@users.noreply.github.com> Date: Thu, 28 Aug 2025 06:56:39 +0900 Subject: [PATCH 108/150] fix(docs): Fix typo in `fDQn` (#6420) --- cloudinit/config/schemas/schema-cloud-config-v1.json | 2 +- doc/module-docs/cc_phone_home/data.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudinit/config/schemas/schema-cloud-config-v1.json b/cloudinit/config/schemas/schema-cloud-config-v1.json index 5fe97a5f840..f5f3ad9310a 100644 --- a/cloudinit/config/schemas/schema-cloud-config-v1.json +++ b/cloudinit/config/schemas/schema-cloud-config-v1.json @@ -3328,7 +3328,7 @@ "properties": { "manage_etc_hosts": { "default": false, - "description": "Whether to manage ``/etc/hosts`` on the system. If ``true``, render the hosts file using ``/etc/cloud/templates/hosts.tmpl`` replacing ``$hostname`` and ``$fdqn``. If ``localhost``, append a ``127.0.1.1`` entry that resolves from FQDN and hostname every boot. Default: ``false``.", + "description": "Whether to manage ``/etc/hosts`` on the system. If ``true``, render the hosts file using ``/etc/cloud/templates/hosts.tmpl`` replacing ``$hostname`` and ``$fqdn``. If ``localhost``, append a ``127.0.1.1`` entry that resolves from FQDN and hostname every boot. Default: ``false``.", "oneOf": [ { "enum": [ diff --git a/doc/module-docs/cc_phone_home/data.yaml b/doc/module-docs/cc_phone_home/data.yaml index 2a34f7996fe..1391aff17b6 100644 --- a/doc/module-docs/cc_phone_home/data.yaml +++ b/doc/module-docs/cc_phone_home/data.yaml @@ -12,7 +12,7 @@ cc_phone_home: - ``pub_key_ed25519`` - ``instance_id`` - ``hostname`` - - ``fdqn`` + - ``fqdn`` Data is sent as ``x-www-form-urlencoded`` arguments. From f9fc38095bb23c75b8917f4989c427c4e72962a7 Mon Sep 17 00:00:00 2001 From: Paul <44974737+paulober@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:58:42 +0100 Subject: [PATCH 109/150] feat(cc_raspberry_pi): remove remote_gpio interface (#6429) Signed-off-by: paulober --- cloudinit/config/cc_raspberry_pi.py | 1 - cloudinit/config/schemas/schema-cloud-config-v1.json | 5 ----- 2 files changed, 6 deletions(-) diff --git a/cloudinit/config/cc_raspberry_pi.py b/cloudinit/config/cc_raspberry_pi.py index a80a8f08b41..2af8db03316 100644 --- a/cloudinit/config/cc_raspberry_pi.py +++ b/cloudinit/config/cc_raspberry_pi.py @@ -22,7 +22,6 @@ "i2c": "do_i2c", "serial": "do_serial", "onewire": "do_onewire", - "remote_gpio": "do_rgpio", } RASPI_CONFIG_SERIAL_CONS_FN = "do_serial_cons" RASPI_CONFIG_SERIAL_HW_FN = "do_serial_hw" diff --git a/cloudinit/config/schemas/schema-cloud-config-v1.json b/cloudinit/config/schemas/schema-cloud-config-v1.json index f5f3ad9310a..b524a162c4e 100644 --- a/cloudinit/config/schemas/schema-cloud-config-v1.json +++ b/cloudinit/config/schemas/schema-cloud-config-v1.json @@ -2692,11 +2692,6 @@ "type": "boolean", "description": "Enable 1-Wire interface. Default: ``false``.", "default": false - }, - "remote_gpio": { - "type": "boolean", - "description": "Enable remote GPIO interface. Default: ``false``.", - "default": false } } }, From e10c8809e8dcf1b05c9d111a2551c33adaf7edbc Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Thu, 28 Aug 2025 05:27:56 -0600 Subject: [PATCH 110/150] feat: support nmap in socket protocol (#6339) Nmap's netcat implementation doesn't support creating a return socket for datagram mode. Switch the socket mode to stream for better compatibility. Fixes GH-6136 --- cloudinit/socket.py | 20 ++++++++------------ systemd/cloud-config.service | 2 +- systemd/cloud-final.service | 2 +- systemd/cloud-init-local.service.tmpl | 2 +- systemd/cloud-init-network.service.tmpl | 2 +- tests/unittests/test_all_stages.py | 3 +-- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/cloudinit/socket.py b/cloudinit/socket.py index 98c82886ff6..0a5485a07e6 100644 --- a/cloudinit/socket.py +++ b/cloudinit/socket.py @@ -5,6 +5,7 @@ import socket import sys from contextlib import suppress +from typing import Dict from cloudinit import performance from cloudinit.settings import DEFAULT_RUN_DIR @@ -55,16 +56,16 @@ def __init__(self, *names: str): :param names: stage names, used as a unique identifiers """ self.stage = "" - self.remote = "" self.first_exception = "" self.systemd_exit_code = 0 self.experienced_any_error = False self.sockets = { name: socket.socket( - socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC + socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_CLOEXEC ) for name in names } + self.connections: Dict[str, socket.socket] = {} # ensure the directory exists os.makedirs(f"{DEFAULT_RUN_DIR}/share", mode=0o700, exist_ok=True) # removing stale sockets and bind @@ -73,6 +74,7 @@ def __init__(self, *names: str): with suppress(FileNotFoundError): os.remove(socket_path) sock.bind(socket_path) + sock.listen() def __call__(self, stage: str): """Set the stage before entering context. @@ -116,19 +118,14 @@ def __enter__(self): # reply, which is expected to be /path/to/{self.stage}-return.sock sock = self.sockets[self.stage] with performance.Timed(f"Waiting to start stage {self.stage}"): - chunk, self.remote = sock.recvfrom(5) + connection, _ = sock.accept() + chunk, _ = connection.recvfrom(5) + self.connections[self.stage] = connection if b"start" != chunk: # The protocol expects to receive a command "start" self.__exit__(None, None, None) raise ValueError(f"Received invalid message: [{str(chunk)}]") - elif f"{DEFAULT_RUN_DIR}/share/{self.stage}-return.sock" != str( - self.remote - ): - # assert that the return path is in a directory with appropriate - # permissions - self.__exit__(None, None, None) - raise ValueError(f"Unexpected path to unix socket: {self.remote}") sd_notify(f"STATUS=Running ({self.stage} stage)") return self @@ -156,8 +153,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.experienced_any_error = self.experienced_any_error or bool( self.systemd_exit_code ) - sock = self.sockets[self.stage] - sock.connect(self.remote) + sock = self.connections[self.stage] # the returned message will be executed in a subshell # hardcode this message rather than sending a more informative message diff --git a/systemd/cloud-config.service b/systemd/cloud-config.service index 68f80d2b3f8..3fe62f9d961 100644 --- a/systemd/cloud-config.service +++ b/systemd/cloud-config.service @@ -16,7 +16,7 @@ Type=oneshot # process has completed this stage. The output from the return socket is piped # into a shell so that the process can send a completion message (defaults to # "done", otherwise includes an error message) and an exit code to systemd. -ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/config.sock -s /run/cloud-init/share/config-return.sock | sh' +ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/config.sock | sh' RemainAfterExit=yes TimeoutSec=0 diff --git a/systemd/cloud-final.service b/systemd/cloud-final.service index fb74a47c8eb..e7e892ab9a4 100644 --- a/systemd/cloud-final.service +++ b/systemd/cloud-final.service @@ -19,7 +19,7 @@ Type=oneshot # process has completed this stage. The output from the return socket is piped # into a shell so that the process can send a completion message (defaults to # "done", otherwise includes an error message) and an exit code to systemd. -ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/final.sock -s /run/cloud-init/share/final-return.sock | sh' +ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/final.sock | sh' RemainAfterExit=yes TimeoutSec=0 TasksMax=infinity diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl index 26a6aee1d05..b8a2f33111c 100644 --- a/systemd/cloud-init-local.service.tmpl +++ b/systemd/cloud-init-local.service.tmpl @@ -33,7 +33,7 @@ ExecStartPre=/sbin/restorecon /run/cloud-init # process has completed this stage. The output from the return socket is piped # into a shell so that the process can send a completion message (defaults to # "done", otherwise includes an error message) and an exit code to systemd. -ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/local.sock -s /run/cloud-init/share/local-return.sock | sh' +ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/local.sock | sh' RemainAfterExit=yes TimeoutSec=0 diff --git a/systemd/cloud-init-network.service.tmpl b/systemd/cloud-init-network.service.tmpl index 61425b4a9fd..9658af1d633 100644 --- a/systemd/cloud-init-network.service.tmpl +++ b/systemd/cloud-init-network.service.tmpl @@ -56,7 +56,7 @@ Type=oneshot # process has completed this stage. The output from the return socket is piped # into a shell so that the process can send a completion message (defaults to # "done", otherwise includes an error message) and an exit code to systemd. -ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/network.sock -s /run/cloud-init/share/network-return.sock | sh' +ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/network.sock | sh' RemainAfterExit=yes TimeoutSec=0 diff --git a/tests/unittests/test_all_stages.py b/tests/unittests/test_all_stages.py index 90bde5e1add..1b66e6955ba 100644 --- a/tests/unittests/test_all_stages.py +++ b/tests/unittests/test_all_stages.py @@ -15,9 +15,8 @@ class Sync: """ def __init__(self, name: str, path: str): - self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(f"{path}/share/{name}.sock") - self.sock.bind(f"{path}/share/{name}-return.sock") self.sock.sendall(b"start") def receive(self): From 63d5b797db4181ea0c715d8c94f2dccc83177571 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 29 Aug 2025 09:10:18 -0500 Subject: [PATCH 111/150] chore: hand off responsibilities (#6432) The faults of incompetent abilities will be consigned to oblivion --- .github/workflows/stale.yml | 4 ++-- doc/rtd/development/first_PR.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c11c2d5de67..b0dee899e2f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: stale-pr-message: | Hello! Thank you for this proposed change to cloud-init. This pull request is now marked as stale as it has not seen any activity in 14 days. If no activity occurs within the next 7 days, this pull request will automatically close. - If you are waiting for code review and you are seeing this message, apologies! Please reply, tagging TheRealFalcon, and he will ensure that someone takes a look soon. + If you are waiting for code review and you are seeing this message, apologies! Please reply, tagging blackboxsw, and he will ensure that someone takes a look soon. - (If the pull request is closed and you would like to continue working on it, please do tag TheRealFalcon to reopen it.) + (If the pull request is closed and you would like to continue working on it, please do tag blackboxsw to reopen it.) stale-pr-label: 'stale-pr' diff --git a/doc/rtd/development/first_PR.rst b/doc/rtd/development/first_PR.rst index 0512a3bb31e..39244f22d1c 100644 --- a/doc/rtd/development/first_PR.rst +++ b/doc/rtd/development/first_PR.rst @@ -18,7 +18,7 @@ To contribute to cloud-init, you must first sign the Canonical every pull request to ensure that the CLA has been signed. For any questions or help with the process, email -`James Falcon `_ with the subject: +`Chad Smith `_ with the subject: "Cloud-init CLA". Create a sandbox environment From 12ab35987e88409623e8762d80e0cdc42b77c6c6 Mon Sep 17 00:00:00 2001 From: Aamir <114517832+Aamir017@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:54:20 +0530 Subject: [PATCH 112/150] tests: convert util.pathprefix2dict tests from unittest to pytest (#6433) --- tests/unittests/test_pathprefix2dict.py | 45 ++++++++++++------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/tests/unittests/test_pathprefix2dict.py b/tests/unittests/test_pathprefix2dict.py index 997061bc1de..834327a0195 100644 --- a/tests/unittests/test_pathprefix2dict.py +++ b/tests/unittests/test_pathprefix2dict.py @@ -1,43 +1,40 @@ # This file is part of cloud-init. See LICENSE file for license information. -import shutil -import tempfile +import pytest from cloudinit import util -from tests.unittests.helpers import TestCase, populate_dir +from tests.unittests.helpers import populate_dir -class TestPathPrefix2Dict(TestCase): - def setUp(self): - super(TestPathPrefix2Dict, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) - - def test_required_only(self): +class TestPathPrefix2Dict: + def test_required_only(self, tmp_path): dirdata = {"f1": b"f1content", "f2": b"f2content"} - populate_dir(self.tmp, dirdata) + populate_dir(str(tmp_path), dirdata) - ret = util.pathprefix2dict(self.tmp, required=["f1", "f2"]) - self.assertEqual(dirdata, ret) + ret = util.pathprefix2dict(str(tmp_path), required=["f1", "f2"]) + assert dirdata == ret - def test_required_missing(self): + def test_required_missing(self, tmp_path): dirdata = {"f1": b"f1content"} - populate_dir(self.tmp, dirdata) + populate_dir(str(tmp_path), dirdata) kwargs = {"required": ["f1", "f2"]} - self.assertRaises(ValueError, util.pathprefix2dict, self.tmp, **kwargs) + with pytest.raises(ValueError): + util.pathprefix2dict(str(tmp_path), **kwargs) - def test_no_required_and_optional(self): + def test_no_required_and_optional(self, tmp_path): dirdata = {"f1": b"f1c", "f2": b"f2c"} - populate_dir(self.tmp, dirdata) + populate_dir(str(tmp_path), dirdata) ret = util.pathprefix2dict( - self.tmp, required=None, optional=["f1", "f2"] + str(tmp_path), required=None, optional=["f1", "f2"] ) - self.assertEqual(dirdata, ret) + assert dirdata == ret - def test_required_and_optional(self): + def test_required_and_optional(self, tmp_path): dirdata = {"f1": b"f1c", "f2": b"f2c"} - populate_dir(self.tmp, dirdata) + populate_dir(str(tmp_path), dirdata) - ret = util.pathprefix2dict(self.tmp, required=["f1"], optional=["f2"]) - self.assertEqual(dirdata, ret) + ret = util.pathprefix2dict( + str(tmp_path), required=["f1"], optional=["f2"] + ) + assert dirdata == ret From f123b65e7713c06eb180ccc449a11f9125253187 Mon Sep 17 00:00:00 2001 From: Chijioke Ibekwe <70472977+chijioke-ibekwe@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:28:27 +0100 Subject: [PATCH 113/150] test: refactor test_data.py to use only pytest (#6440) Remove helpers.TestCase inheritance and replace self.assert methods with bare assert statements. This change is a refactor only. Test behavior and coverage remains exactly the same. GH-6427 --- tests/unittests/test_data.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index 204765e4874..7861afc2019 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -23,7 +23,6 @@ from cloudinit import util from cloudinit.config.modules import Modules from cloudinit.settings import DEFAULT_RUN_DIR, PER_INSTANCE -from tests.unittests import helpers from tests.unittests.util import FakeDataSource MPATH = "cloudinit.stages" @@ -790,29 +789,29 @@ def test_compressed_in_userdata(self, ud_proc): assert count_messages(message) == 1 -class TestConvertString(helpers.TestCase): +class TestConvertString: def test_handles_binary_non_utf8_decodable(self): """Printable unicode (not utf8-decodable) is safely converted.""" blob = b"#!/bin/bash\necho \xc3\x84\n" msg = ud.convert_string(blob) - self.assertEqual(blob, msg.get_payload(decode=True)) + assert blob == msg.get_payload(decode=True) def test_handles_binary_utf8_decodable(self): blob = b"\x32\x32" msg = ud.convert_string(blob) - self.assertEqual(blob, msg.get_payload(decode=True)) + assert blob == msg.get_payload(decode=True) def test_handle_headers(self): text = "hi mom" msg = ud.convert_string(text) - self.assertEqual(text, msg.get_payload(decode=False)) + assert text == msg.get_payload(decode=False) def test_handle_mime_parts(self): """Mime parts are properly returned as a mime message.""" message = MIMEBase("text", "plain") message.set_payload("Just text") msg = ud.convert_string(str(message)) - self.assertEqual("Just text", msg.get_payload(decode=False)) + assert "Just text" == msg.get_payload(decode=False) class TestFetchBaseConfig: From 464903f9625b19aec82f1d3176a28b8b610a5532 Mon Sep 17 00:00:00 2001 From: Paul <44974737+paulober@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:27:53 +0100 Subject: [PATCH 114/150] docs(cc_raspberry_pi): Fix example4 (#6439) Signed-off-by: paulober --- doc/module-docs/cc_raspberry_pi/example4.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/module-docs/cc_raspberry_pi/example4.yaml b/doc/module-docs/cc_raspberry_pi/example4.yaml index c600a276ffc..9e83ca26456 100644 --- a/doc/module-docs/cc_raspberry_pi/example4.yaml +++ b/doc/module-docs/cc_raspberry_pi/example4.yaml @@ -1,7 +1,6 @@ #cloud-config rpi: interfaces: - ssh: true # works on all Pi models # only enables the UART hardware without binding it to the console serial: From 6f27d0e8dbbb74ef34579d283621da82ebf3ba26 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Tue, 2 Sep 2025 15:36:55 +0200 Subject: [PATCH 115/150] test: fix lru_cache test wrapper (#6443) The position of the cached func in the decorator and decorated signatures varies depending on how the decorator is called (with or without args). Fix the latter case. Fixes tox -e hypothesis-slow. Fixes GH-5869 --- tests/unittests/early_patches.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unittests/early_patches.py b/tests/unittests/early_patches.py index 01b894d3e0a..3848263e9f0 100644 --- a/tests/unittests/early_patches.py +++ b/tests/unittests/early_patches.py @@ -6,8 +6,15 @@ def wrapped_lru_cache(*args, **kwargs): - def wrapper(func: Callable, *a, **k): - new_func = old_lru_cache(*args, **kwargs)(func, *a, **k) + def wrapper(*a, **k): + func: Callable + if len(args) > 0 and callable(args[0]): + func = args[0] + elif len(a) > 0 and callable(a[0]): + func = a[0] + else: + raise NotImplementedError("cannot find cached func") + new_func = old_lru_cache(*args, **kwargs)(*a, **k) # Without this check, we'll also store stdlib functions with @lru_cache if "cloudinit" in func.__module__: From ada8b0cb73fb53d5f5375cf6c697405b62201d89 Mon Sep 17 00:00:00 2001 From: dermotbradley Date: Tue, 2 Sep 2025 16:04:11 +0100 Subject: [PATCH 116/150] net_convert.py: make some import failures not generate an error (#6399) If cloud-init DataSources are separately packaged by a distro then it is possible that running "cloud-init devel net-convert -h" (and trying to convert between some network formats) can fail with errors if Azure, Openstack, or VMware cloud-init code is not present. I observed these errors when running the above command on an Alpine system where cloud-init's various DataSources are separately packaged and only the NoCloud DataSource was installed. This PR prevents import failures of Azure, Openstack, and VMware specific code from causing such errors. Instead the accepted list of inputs ("-k") is dynamically adapted based on the presence/absence of each of the Azure/Openstack/VMware related code. NOTE: I realise that cloud-init does not currently officially support the packaging of DataSources code separately. However I believe that Ubuntu is currently in the process of developing/testing such separate DataSources packaging and so this PR would be relevant to Ubuntu also. --- cloudinit/cmd/devel/net_convert.py | 45 +++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py index b3c2fa7c513..eafb11f16e9 100755 --- a/cloudinit/cmd/devel/net_convert.py +++ b/cloudinit/cmd/devel/net_convert.py @@ -20,9 +20,28 @@ networkd, sysconfig, ) -from cloudinit.sources import DataSourceAzure as azure -from cloudinit.sources.helpers import openstack -from cloudinit.sources.helpers.vmware.imc import guestcust_util + +try: + from cloudinit.sources import DataSourceAzure as azure +except ImportError: + azure_kind_available = False +else: + azure_kind_available = True + +try: + from cloudinit.sources.helpers import openstack +except ImportError: + openstack_kind_available = False +else: + openstack_kind_available = True + +try: + from cloudinit.sources.helpers.vmware.imc import guestcust_util +except ImportError: + vmware_kind_available = False +else: + vmware_kind_available = True + NAME = "net-convert" @@ -45,16 +64,22 @@ def get_parser(parser=None): required=True, help="The network configuration to read", ) + + available_kinds = ["eni", "yaml"] + + if azure_kind_available: + available_kinds.append("azure-imds") + + if openstack_kind_available: + available_kinds.append("network_data.json") + + if vmware_kind_available: + available_kinds.append("vmware-imc") + parser.add_argument( "-k", "--kind", - choices=[ - "eni", - "network_data.json", - "yaml", - "azure-imds", - "vmware-imc", - ], + choices=available_kinds, required=True, help="The format of the given network config", ) From 822358b0c6c294034b2074828d8b86ed4fa70794 Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Tue, 2 Sep 2025 10:20:27 -0600 Subject: [PATCH 117/150] chore: bump isort version (#6446) --- tests/unittests/sources/test_smartos.py | 5 ++++- tox.ini | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unittests/sources/test_smartos.py b/tests/unittests/sources/test_smartos.py index 1f4418eba91..8083df4825c 100644 --- a/tests/unittests/sources/test_smartos.py +++ b/tests/unittests/sources/test_smartos.py @@ -31,7 +31,10 @@ from cloudinit.atomic_helper import b64e from cloudinit.event import EventScope, EventType from cloudinit.sources import DataSourceSmartOS -from cloudinit.sources.DataSourceSmartOS import SERIAL_DEVICE, SMARTOS_ENV_KVM +from cloudinit.sources.DataSourceSmartOS import ( + SERIAL_DEVICE, + SMARTOS_ENV_KVM, +) from cloudinit.sources.DataSourceSmartOS import ( convert_smartos_network_data as convert_net, ) diff --git a/tox.ini b/tox.ini index 3c5c716cb84..d3d877d4410 100644 --- a/tox.ini +++ b/tox.ini @@ -41,7 +41,7 @@ deps = black==25.1.0 hypothesis==6.111.0 hypothesis_jsonschema==0.23.1 - isort==5.13.2 + isort==6.0.1 mypy==1.17.1 pylint==3.3.8 ruff==0.12.9 From 5fd23da6171b63f0e5e10358f2e3ade2f9290e7a Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab <46541787+mostafaCamel@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:14:15 +0300 Subject: [PATCH 118/150] feat(rh_subscription): new release_version field (#6347) Allow setting release major.minor version with subscription-manager Fixes GH-6030 --- cloudinit/config/cc_rh_subscription.py | 41 +++++++++ .../schemas/schema-cloud-config-v1.json | 4 + cloudinit/util.py | 12 ++- .../cc_rh_subscription/example3.yaml | 3 + .../config/test_cc_rh_subscription.py | 91 ++++++++++++++++++- tests/unittests/test_util.py | 40 ++++++++ 6 files changed, 186 insertions(+), 5 deletions(-) diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py index 893ec780fab..cad6e74e061 100644 --- a/cloudinit/config/cc_rh_subscription.py +++ b/cloudinit/config/cc_rh_subscription.py @@ -69,6 +69,10 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: return_stat = sm.update_repos() if not return_stat: raise SubscriptionError("Unable to add or remove repos") + if sm.release_version: + sm._set_release_version() + sm._delete_packagemanager_cache() + sm.log_success("rh_subscription plugin completed successfully") except SubscriptionError as e: sm.log_warn(str(e)) @@ -94,6 +98,7 @@ class SubscriptionManager: "server-hostname", "auto-attach", "service-level", + "release_version", ] def __init__(self, cfg, log=None): @@ -113,6 +118,7 @@ def __init__(self, cfg, log=None): self.enable_repo = self.rhel_cfg.get("enable-repo") self.disable_repo = self.rhel_cfg.get("disable-repo") self.servicelevel = self.rhel_cfg.get("service-level") + self.release_version = self.rhel_cfg.get("release_version") def log_success(self, msg): """Simple wrapper for logging info messages. Useful for unittests""" @@ -155,6 +161,13 @@ def _verify_keys(self): "auto-attach: True" ) return False, no_auto + + # Not verifying the release_version statically in _verify_keys + # (by verifying the key is in the output of + # `subscription-manager release --list`) because sometimes + # the release will become available only after enabling some repos + # (which is executed after verify_keys). So we will catch this error + # during "subscription-manager release --set=" return True, None def is_registered(self): @@ -446,6 +459,34 @@ def update_repos(self): def is_configured(self): return bool((self.userid and self.password) or self.activation_key) + def _set_release_version(self): + """ + Execute "subscription-manager release --set=" + Raises Subscription error if the command fails + """ + + cmd = ["release", f"--set={self.release_version}"] + try: + _sub_man_cli(cmd) + except subp.ProcessExecutionError as e: + raise SubscriptionError( + f"Unable to set release_version using: {cmd}" + ) from e + + def _delete_packagemanager_cache(self): + """ + Delete the package manager cache. + Raises Subscription error if the deletion fails + """ + LOG.debug("Deleting the package manager cache") + try: + util.del_dir("/var/cache/dnf") + util.del_dir("/var/cache/yum") + except Exception as e: + raise SubscriptionError( + "Unable to delete the package manager cache" + ) from e + def _sub_man_cli(cmd, logstring_val=False): """ diff --git a/cloudinit/config/schemas/schema-cloud-config-v1.json b/cloudinit/config/schemas/schema-cloud-config-v1.json index b524a162c4e..435d2ea7d14 100644 --- a/cloudinit/config/schemas/schema-cloud-config-v1.json +++ b/cloudinit/config/schemas/schema-cloud-config-v1.json @@ -2633,6 +2633,10 @@ "type": "string" } }, + "release_version": { + "type": "string", + "description": "Sets the release_version via``subscription-manager release --set=`` then deletes the package manager cache ``/var/cache/{dnf,yum}`` . These steps are applied after any pool attachment and/or enabling/disabling repos. For more information about this key, check https://access.redhat.com/solutions/238533 ." + }, "rhsm-baseurl": { "type": "string", "description": "Sets the baseurl in ``/etc/rhsm/rhsm.conf``." diff --git a/cloudinit/util.py b/cloudinit/util.py index 348cd863f12..3e65c7ef508 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -914,8 +914,18 @@ def center(text, fill, max_len): def del_dir(path): + ''' + Deletes a directory and all its contents by calling shutil.rmtree + Will ignore FileNotFoundError + + @param path: The path of the directory. + """ + ''' LOG.debug("Recursively deleting %s", path) - shutil.rmtree(path) + try: + shutil.rmtree(path) + except FileNotFoundError: + pass def read_optional_seed(fill, base="", ext="", timeout=5): diff --git a/doc/module-docs/cc_rh_subscription/example3.yaml b/doc/module-docs/cc_rh_subscription/example3.yaml index a8b741ccd2d..1b12c6171d3 100644 --- a/doc/module-docs/cc_rh_subscription/example3.yaml +++ b/doc/module-docs/cc_rh_subscription/example3.yaml @@ -17,3 +17,6 @@ rh_subscription: rhsm-baseurl: http://url # Alter the server hostname in /etc/rhsm/rhsm.conf server-hostname: foo.bar.com + # Set `subscription-manager release --set=6Server` then + # delete /var/cache/{dnf,yum} + release_version: 6Server diff --git a/tests/unittests/config/test_cc_rh_subscription.py b/tests/unittests/config/test_cc_rh_subscription.py index 0f4676170d6..98d8e86f1c8 100644 --- a/tests/unittests/config/test_cc_rh_subscription.py +++ b/tests/unittests/config/test_cc_rh_subscription.py @@ -40,6 +40,7 @@ class TestHappyPath: "add-pool": ["pool1", "pool2", "pool3"], "enable-repo": ["repo1", "repo2", "repo3"], "disable-repo": ["repo4", "repo5"], + "release_version": "7.6b", } } @@ -52,7 +53,12 @@ def test_already_registered(self, m_sman_cli, caplog): assert m_sman_cli.call_count == 1 assert "System is already registered" in caplog.text - def test_simple_registration(self, m_sman_cli, caplog): + @mock.patch.object( + cc_rh_subscription.SubscriptionManager, "_set_release_version" + ) + def test_simple_registration( + self, m_set_release_version, m_sman_cli, caplog + ): """ Simple registration with username and password """ @@ -76,6 +82,7 @@ def test_simple_registration(self, m_sman_cli, caplog): ) assert "rh_subscription plugin completed successfully" in caplog.text assert m_sman_cli.call_count == 2 + assert m_set_release_version.call_count == 0 @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_getRepos") def test_update_repos_disable_with_none(self, m_get_repos, m_sman_cli): @@ -94,7 +101,7 @@ def test_update_repos_disable_with_none(self, m_get_repos, m_sman_cli): def test_full_registration(self, m_sman_cli, caplog): """ Registration with auto-attach, service-level, adding pools, - and enabling and disabling yum repos + enabling and disabling yum repos and setting release_version """ call_lists = [] call_lists.append(["attach", "--pool=pool1", "--pool=pool3"]) @@ -102,6 +109,7 @@ def test_full_registration(self, m_sman_cli, caplog): ["repos", "--disable=repo5", "--enable=repo2", "--enable=repo3"] ) call_lists.append(["attach", "--auto", "--servicelevel=self-support"]) + call_lists.append(["release", "--set=7.6b"]) reg = ( "The system has been registered with ID:" " 12345678-abde-abcde-1234-1234567890abc" @@ -116,9 +124,15 @@ def test_full_registration(self, m_sman_cli, caplog): ("Repo ID: repo1\nRepo ID: repo5\n", ""), ("Repo ID: repo2\nRepo ID: repo3\nRepo ID: repo4", ""), ("", ""), + ("Release set to: 7.6b", ""), ] + # to avoid deleting the actual cache files + # (triggered by the presence of the release_version key) + # on the host running the tests + mock.patch("shutil.rmtree") + cc_rh_subscription.handle(NAME, self.CONFIG_FULL, None, []) - assert m_sman_cli.call_count == 9 + assert m_sman_cli.call_count == 10 for call in call_lists: assert mock.call(call) in m_sman_cli.call_args_list assert "rh_subscription plugin completed successfully" in caplog.text @@ -169,6 +183,13 @@ class TestBadInput: "org": "ABC", } } + CONFIG_BAD_RELEASE_VERSION = { + "rh_subscription": { + "username": "scooby@do.com", + "password": "scooby-snacks", + "release_version": "bad_release_version", + } + } def assert_logged_warnings(self, warnings, caplog): missing = [ @@ -272,7 +293,65 @@ def test_bad_key_value(self, m_sman_cli, caplog): "fookey is not a valid key for rh_subscription. Valid keys" " are: org, activation-key, username, password, disable-repo," " enable-repo, add-pool, rhsm-baseurl, server-hostname," - " auto-attach, service-level", + " auto-attach, service-level, release_version", + "rh_subscription plugin did not complete successfully", + ), + caplog, + ) + + @mock.patch.object( + cc_rh_subscription.SubscriptionManager, "_delete_packagemanager_cache" + ) + def test_bad_release_version(self, m_delete_pm_cache, m_sman_cli, caplog): + """ + Failure at setting release_version + """ + m_sman_cli.side_effect = [ + subp.ProcessExecutionError, + (self.REG, "bar"), + subp.ProcessExecutionError, + ] + cc_rh_subscription.handle( + NAME, self.CONFIG_BAD_RELEASE_VERSION, None, [] + ) + assert m_sman_cli.call_count == 3 + assert m_delete_pm_cache.call_count == 0 + expected_cmd = [ + "release", + f"--set={self.CONFIG_BAD_RELEASE_VERSION['rh_subscription']['release_version']}", + ] + self.assert_logged_warnings( + ( + f"Unable to set release_version using: {expected_cmd}", + "rh_subscription plugin did not complete successfully", + ), + caplog, + ) + + @mock.patch("shutil.rmtree", side_effect=[PermissionError]) + def test_pm_cache_deletion_after_setting_release_version( + self, m_rmtree, m_sman_cli, caplog + ): + """ + Failure at deleting package manager cache + after setting release_version + """ + good_release_ver_cfg = copy.deepcopy(self.CONFIG_BAD_RELEASE_VERSION) + good_release_ver_cfg["rh_subscription"][ + "release_version" + ] = "1.2Server" + m_sman_cli.side_effect = [ + subp.ProcessExecutionError, + (self.REG, "bar"), + ("Release set to: 1.2Server", ""), + ] + cc_rh_subscription.handle(NAME, good_release_ver_cfg, None, []) + # assert "rh_subscription plugin completed successfully" in caplog.text + assert m_sman_cli.call_count == 3 + assert m_rmtree.call_args_list == [mock.call("/var/cache/dnf")] + self.assert_logged_warnings( + ( + "Unable to delete the package manager cache", "rh_subscription plugin did not complete successfully", ), caplog, @@ -299,6 +378,10 @@ class TestRhSubscriptionSchema: {"rh_subscription": {"disable-repo": "name"}}, "'name' is not of type 'array'", ), + ( + {"rh_subscription": {"release_version": [10]}}, + r"\[10\] is not of type 'string'", + ), ( { "rh_subscription": { diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index 0a6add54cc4..2651e2ff619 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -13,6 +13,7 @@ import stat import tempfile from collections import deque +from contextlib import nullcontext as does_not_raise from pathlib import Path from textwrap import dedent from unittest import mock @@ -2027,6 +2028,45 @@ def test_deletes_symlinks(self): self.assertDirEmpty(self.tmp) +class TestDelDir: + """ + Test the del_dir function + """ + + def test_del_dir_existing_directory(self, tmpdir): + """ + An existing directory can be deleted without issues + """ + assert os.path.exists(tmpdir) + with does_not_raise(): + util.del_dir(tmpdir) + assert not os.path.exists(tmpdir) + + def test_del_dir_file_not_found(self): + """ + Should not raise FileNotFoundError + """ + non_existing_dir = "/blabla" + assert not os.path.exists(non_existing_dir) + with does_not_raise(): + util.del_dir(non_existing_dir) + assert not os.path.exists(non_existing_dir) + + def test_del_dir_generic_errors(self, mocker): + """ + If shutil.rmtree raises a non-FileNotFoundError , del_dir should + raise this error + """ + mocked_side_effect = PermissionError + mock_rmtree = mocker.patch( + "shutil.rmtree", + side_effect=mocked_side_effect, + ) + with pytest.raises(mocked_side_effect): + util.del_dir("somedir") + assert mock_rmtree.call_count == 1 + + class TestKeyValStrings(helpers.TestCase): def test_keyval_str_to_dict(self): expected = {"1": "one", "2": "one+one", "ro": True} From 8d888b336fcfee4bf3e2cc818fff9ca3720ecda3 Mon Sep 17 00:00:00 2001 From: dermotbradley Date: Tue, 2 Sep 2025 21:23:22 +0100 Subject: [PATCH 119/150] fix: add OpenRC support to Meson build (#6426) Modify meson.build to install relevant files for OpenRC Also modify 2 OpenRC scripts to run required scripts from /usr/libexec path rather than /usr/lib. --- meson.build | 26 ++++++++++++++++++++++++++ sysvinit/openrc/cloud-init-ds-identify | 2 +- sysvinit/openrc/cloud-init-hotplug | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 5b1851dbe1d..16a855bdfa8 100644 --- a/meson.build +++ b/meson.build @@ -157,6 +157,32 @@ if init_system == 'systemd' install_mode: 'rw-r--r--', install_tag: 'systemd', ) +elif init_system == 'sysvinit_openrc' + openrc = dependency('openrc') + udev = dependency('udev') + udev_dir = udev.get_variable(pkgconfig: 'udevdir') + install_data( + 'udev/66-azure-ephemeral.rules', + install_dir: udev_dir / 'rules.d', + install_mode: 'rw-r--r--', + install_tag: 'openrc', + ) + + install_data( + run_command(find, 'sysvinit/openrc', '-type', 'f', check: true).stdout().strip().split('\n'), + install_dir: '/etc/init.d', + install_mode: 'rwxr-xr-x', + install_tag: 'openrc', + ) + + install_data( + [ + 'tools/cloud-init-hotplugd', + ], + install_dir: lib_exec_dir, + install_mode: 'rwxr-xr-x', + install_tag: 'bin', + ) endif custom_target( diff --git a/sysvinit/openrc/cloud-init-ds-identify b/sysvinit/openrc/cloud-init-ds-identify index eeca565954c..41f5435fe57 100755 --- a/sysvinit/openrc/cloud-init-ds-identify +++ b/sysvinit/openrc/cloud-init-ds-identify @@ -15,7 +15,7 @@ start() { ewarn "$RC_SVCNAME is disabled via cloud-init.disabled file" else ebegin "$description" - /usr/lib/cloud-init/ds-identify + /usr/libexec/cloud-init/ds-identify eend $? fi } diff --git a/sysvinit/openrc/cloud-init-hotplug b/sysvinit/openrc/cloud-init-hotplug index a45720ec7dc..cd37b3fb74b 100755 --- a/sysvinit/openrc/cloud-init-hotplug +++ b/sysvinit/openrc/cloud-init-hotplug @@ -2,7 +2,7 @@ description="cloud-init hotplug daemon" -command="/usr/lib/cloud-init/cloud-init-hotplugd" +command="/usr/libexec/cloud-init/cloud-init-hotplugd" pidfile="/run/$RC_SVCNAME.pid" depend() { From c5c635847a78819a526d61d7bf1ed03c4abce63a Mon Sep 17 00:00:00 2001 From: ExplGamma <36189127+ExplGamma@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:24:17 +1000 Subject: [PATCH 120/150] Update wait_for_cloud_init.rst to be more consistent about systemd usage (#6442) Correcting minor documentation inconsistency. --- doc/rtd/howto/wait_for_cloud_init.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/rtd/howto/wait_for_cloud_init.rst b/doc/rtd/howto/wait_for_cloud_init.rst index 929ab138593..0ca2ca33c22 100644 --- a/doc/rtd/howto/wait_for_cloud_init.rst +++ b/doc/rtd/howto/wait_for_cloud_init.rst @@ -23,7 +23,7 @@ completes. This may be accomplished by including [Unit] Description=Example service - After=cloud-final.service multi-user.target + After=cloud-init.target multi-user.target [Service] Type=oneshot From cd03bf97c612b1831bd98ba2b7b461471c35858d Mon Sep 17 00:00:00 2001 From: Chijioke Ibekwe <70472977+chijioke-ibekwe@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:46:31 +0100 Subject: [PATCH 121/150] test: refactor test_reporting.py to use only pytest (#6449) Remove helpers.TestCase inheritance, replace unittest assertions with pytest equivalents, and rename helper methods using pytest naming convention. This change is a refactor only. Test behavior and coverage remains exactly the same. GH-6427 --- tests/unittests/reporting/test_reporting.py | 209 +++++++++----------- 1 file changed, 89 insertions(+), 120 deletions(-) diff --git a/tests/unittests/reporting/test_reporting.py b/tests/unittests/reporting/test_reporting.py index 9ed82583d7e..ad0f16b0f7a 100644 --- a/tests/unittests/reporting/test_reporting.py +++ b/tests/unittests/reporting/test_reporting.py @@ -13,8 +13,8 @@ get_schema, validate_cloudconfig_schema, ) -from cloudinit.reporting import events, handlers -from tests.unittests.helpers import TestCase, skipUnlessJsonSchema +from cloudinit.reporting import events +from tests.unittests.helpers import skipUnlessJsonSchema def _fake_registry(): @@ -23,7 +23,7 @@ def _fake_registry(): ) -class TestReportStartEvent(TestCase): +class TestReportStartEvent: @mock.patch( "cloudinit.reporting.events.instantiated_handler_registry", new_callable=_fake_registry, @@ -40,12 +40,12 @@ def test_report_start_event_passes_something_with_as_string_to_handlers( _, handler, ) in instantiated_handler_registry.registered_items.items(): - self.assertEqual(1, handler.publish_event.call_count) + assert handler.publish_event.call_count == 1 event = handler.publish_event.call_args[0][0] - self.assertEqual(expected_string_representation, event.as_string()) + assert expected_string_representation == event.as_string() -class TestReportFinishEvent(TestCase): +class TestReportFinishEvent: def _report_finish_event(self, result=events.status.SUCCESS): event_name, event_description = "my_test_event", "my description" events.report_finish_event( @@ -53,13 +53,13 @@ def _report_finish_event(self, result=events.status.SUCCESS): ) return event_name, event_description - def assertHandlersPassedObjectWithAsString( + def assert_handlers_passed_object_with_as_string( self, handlers, expected_as_string ): for _, handler in handlers.items(): - self.assertEqual(1, handler.publish_event.call_count) + assert handler.publish_event.call_count == 1 event = handler.publish_event.call_args[0][0] - self.assertEqual(expected_as_string, event.as_string()) + assert expected_as_string == event.as_string() @mock.patch( "cloudinit.reporting.events.instantiated_handler_registry", @@ -72,7 +72,7 @@ def test_report_finish_event_passes_something_with_as_string_to_handlers( expected_string_representation = ": ".join( ["finish", event_name, events.status.SUCCESS, event_description] ) - self.assertHandlersPassedObjectWithAsString( + self.assert_handlers_passed_object_with_as_string( instantiated_handler_registry.registered_items, expected_string_representation, ) @@ -90,7 +90,7 @@ def test_reporting_successful_finish_has_sensible_string_repr( expected_string_representation = ": ".join( ["finish", event_name, events.status.SUCCESS, event_description] ) - self.assertHandlersPassedObjectWithAsString( + self.assert_handlers_passed_object_with_as_string( instantiated_handler_registry.registered_items, expected_string_representation, ) @@ -108,23 +108,24 @@ def test_reporting_unsuccessful_finish_has_sensible_string_repr( expected_string_representation = ": ".join( ["finish", event_name, events.status.FAIL, event_description] ) - self.assertHandlersPassedObjectWithAsString( + self.assert_handlers_passed_object_with_as_string( instantiated_handler_registry.registered_items, expected_string_representation, ) def test_invalid_result_raises_attribute_error(self): - self.assertRaises(ValueError, self._report_finish_event, ("BOGUS",)) + with pytest.raises(ValueError): + self._report_finish_event("BOGUS") -class TestReportingEvent(TestCase): +class TestReportingEvent: def test_as_string(self): event_type, name, description = "test_type", "test_name", "test_desc" event = events.ReportingEvent(event_type, name, description) expected_string_representation = ": ".join( [event_type, name, description] ) - self.assertEqual(expected_string_representation, event.as_string()) + assert expected_string_representation == event.as_string() def test_as_dict(self): event_type, name, desc = "test_type", "test_name", "test_desc" @@ -138,20 +139,20 @@ def test_as_dict(self): # allow for timestamp to differ, but must be present as_dict = event.as_dict() - self.assertIn("timestamp", as_dict) + assert "timestamp" in as_dict del as_dict["timestamp"] - self.assertEqual(expected, as_dict) + assert expected == as_dict -class TestFinishReportingEvent(TestCase): +class TestFinishReportingEvent: def test_as_has_result(self): result = events.status.SUCCESS name, desc = "test_name", "test_desc" event = events.FinishReportingEvent(name, desc, result) ret = event.as_dict() - self.assertTrue("result" in ret) - self.assertEqual(ret["result"], result) + assert "result" in ret + assert ret["result"] == result def test_has_result_with_optional_post_files(self): result = events.status.SUCCESS @@ -164,56 +165,43 @@ def test_has_result_with_optional_post_files(self): name, desc, result, post_files=files ) ret = event.as_dict() - self.assertTrue("result" in ret) - self.assertTrue("files" in ret) - self.assertEqual(ret["result"], result) + assert "result" in ret + assert "files" in ret + assert ret["result"] == result posted_install_log = ret["files"][0] - self.assertTrue("path" in posted_install_log) - self.assertTrue("content" in posted_install_log) - self.assertTrue("encoding" in posted_install_log) - self.assertEqual(posted_install_log["path"], files[0]) - self.assertEqual(posted_install_log["encoding"], "base64") + assert "path" in posted_install_log + assert "content" in posted_install_log + assert "encoding" in posted_install_log + assert posted_install_log["path"] == files[0] + assert posted_install_log["encoding"] == "base64" -class TestBaseReportingHandler(TestCase): - def test_base_reporting_handler_is_abstract(self): - regexp = r".*abstract.*publish_event.*" - self.assertRaisesRegex(TypeError, regexp, handlers.ReportingHandler) - - -class TestLogHandler(TestCase): +class TestLogHandler: @mock.patch.object(reporting.handlers.logging, "getLogger") def test_appropriate_logger_used(self, getLogger): event_type, event_name = "test_type", "test_name" event = events.ReportingEvent(event_type, event_name, "description") reporting.handlers.LogHandler().publish_event(event) - self.assertEqual( - [ - mock.call( - "cloudinit.reporting.{0}.{1}".format( - event_type, event_name - ) - ) - ], - getLogger.call_args_list, - ) + assert getLogger.call_args_list == [ + mock.call( + "cloudinit.reporting.{0}.{1}".format(event_type, event_name) + ) + ] @mock.patch.object(reporting.handlers.logging, "getLogger") def test_single_log_message_at_info_published(self, getLogger): event = events.ReportingEvent("type", "name", "description") reporting.handlers.LogHandler().publish_event(event) - self.assertEqual(1, getLogger.return_value.log.call_count) + assert getLogger.return_value.log.call_count == 1 @mock.patch.object(reporting.handlers.logging, "getLogger") def test_log_message_uses_event_as_string(self, getLogger): event = events.ReportingEvent("type", "name", "description") reporting.handlers.LogHandler(level="INFO").publish_event(event) - self.assertIn( - event.as_string(), getLogger.return_value.log.call_args[0][1] - ) + assert event.as_string() in getLogger.return_value.log.call_args[0][1] -class TestDefaultRegisteredHandler(TestCase): +class TestDefaultRegisteredHandler: def test_log_handler_registered_by_default(self): registered_items = ( reporting.instantiated_handler_registry.registered_items @@ -222,18 +210,16 @@ def test_log_handler_registered_by_default(self): if isinstance(item, reporting.handlers.LogHandler): break else: - self.fail("No reporting LogHandler registered by default.") + pytest.fail("No reporting LogHandler registered by default.") -class TestReportingConfiguration(TestCase): +class TestReportingConfiguration: @mock.patch.object(reporting, "instantiated_handler_registry") def test_empty_configuration_doesnt_add_handlers( self, instantiated_handler_registry ): reporting.update_configuration({}) - self.assertEqual( - 0, instantiated_handler_registry.register_item.call_count - ) + assert instantiated_handler_registry.register_item.call_count == 0 @mock.patch.object( reporting, "instantiated_handler_registry", reporting.DictRegistry() @@ -247,10 +233,9 @@ def test_looks_up_handler_by_type_and_adds_it(self, available_handlers): reporting.update_configuration( {handler_name: {"type": handler_type_name}} ) - self.assertEqual( - {handler_name: handler_cls.return_value}, - reporting.instantiated_handler_registry.registered_items, - ) + assert reporting.instantiated_handler_registry.registered_items == { + handler_name: handler_cls.return_value + } @mock.patch.object( reporting, "instantiated_handler_registry", reporting.DictRegistry() @@ -267,15 +252,13 @@ def test_uses_non_type_parts_of_config_dict_as_kwargs( handler_config.update({"type": handler_type_name}) handler_name = "my_test_handler" reporting.update_configuration({handler_name: handler_config}) - self.assertEqual( - handler_cls.return_value, + assert ( reporting.instantiated_handler_registry.registered_items[ handler_name - ], - ) - self.assertEqual( - [mock.call(**extra_kwargs)], handler_cls.call_args_list + ] + == handler_cls.return_value ) + assert handler_cls.call_args_list == [mock.call(**extra_kwargs)] @mock.patch.object( reporting, "instantiated_handler_registry", reporting.DictRegistry() @@ -288,7 +271,7 @@ def test_handler_config_not_modified(self, available_handlers): handler_config = {"type": handler_type_name, "foo": "bar"} expected_handler_config = handler_config.copy() reporting.update_configuration({"my_test_handler": handler_config}) - self.assertEqual(expected_handler_config, handler_config) + assert expected_handler_config == handler_config @mock.patch.object( reporting, "instantiated_handler_registry", reporting.DictRegistry() @@ -302,32 +285,25 @@ def test_handlers_removed_if_falseish_specified(self, available_handlers): reporting.update_configuration( {handler_name: {"type": handler_type_name}} ) - self.assertEqual( - 1, len(reporting.instantiated_handler_registry.registered_items) + assert ( + len(reporting.instantiated_handler_registry.registered_items) == 1 ) reporting.update_configuration({handler_name: None}) - self.assertEqual( - 0, len(reporting.instantiated_handler_registry.registered_items) + assert ( + len(reporting.instantiated_handler_registry.registered_items) == 0 ) -class TestReportingEventStack(TestCase): +class TestReportingEventStack: @mock.patch("cloudinit.reporting.events.report_finish_event") @mock.patch("cloudinit.reporting.events.report_start_event") def test_start_and_finish_success(self, report_start, report_finish): with events.ReportEventStack(name="myname", description="mydesc"): pass - self.assertEqual( - [mock.call("myname", "mydesc")], report_start.call_args_list - ) - self.assertEqual( - [ - mock.call( - "myname", "mydesc", events.status.SUCCESS, post_files=[] - ) - ], - report_finish.call_args_list, - ) + assert report_start.call_args_list == [mock.call("myname", "mydesc")] + assert report_finish.call_args_list == [ + mock.call("myname", "mydesc", events.status.SUCCESS, post_files=[]) + ] @mock.patch("cloudinit.reporting.events.report_finish_event") @mock.patch("cloudinit.reporting.events.report_start_event") @@ -339,11 +315,10 @@ def test_finish_exception_defaults_fail(self, report_start, report_finish): raise ValueError("This didnt work") except ValueError: pass - self.assertEqual([mock.call(name, desc)], report_start.call_args_list) - self.assertEqual( - [mock.call(name, desc, events.status.FAIL, post_files=[])], - report_finish.call_args_list, - ) + assert report_start.call_args_list == [mock.call(name, desc)] + assert report_finish.call_args_list == [ + mock.call(name, desc, events.status.FAIL, post_files=[]) + ] @mock.patch("cloudinit.reporting.events.report_finish_event") @mock.patch("cloudinit.reporting.events.report_start_event") @@ -357,11 +332,10 @@ def test_result_on_exception_used(self, report_start, report_finish): raise ValueError("This didnt work") except ValueError: pass - self.assertEqual([mock.call(name, desc)], report_start.call_args_list) - self.assertEqual( - [mock.call(name, desc, events.status.WARN, post_files=[])], - report_finish.call_args_list, - ) + assert report_start.call_args_list == [mock.call(name, desc)] + assert report_finish.call_args_list == [ + mock.call(name, desc, events.status.WARN, post_files=[]) + ] @mock.patch("cloudinit.reporting.events.report_start_event") def test_child_fullname_respects_parent(self, report_start): @@ -380,8 +354,7 @@ def test_child_fullname_respects_parent(self, report_start): report_start.assert_called_with(c2_expected_fullname, "c2desc") @mock.patch("cloudinit.reporting.events.report_finish_event") - @mock.patch("cloudinit.reporting.events.report_start_event") - def test_child_result_bubbles_up(self, report_start, report_finish): + def test_child_result_bubbles_up(self, report_finish): parent = events.ReportEventStack("topname", "topdesc") child = events.ReportEventStack("c_name", "c_desc", parent=parent) with parent: @@ -396,27 +369,21 @@ def test_child_result_bubbles_up(self, report_start, report_finish): def test_message_used_in_finish(self, report_finish): with events.ReportEventStack("myname", "mydesc", message="mymessage"): pass - self.assertEqual( - [ - mock.call( - "myname", "mymessage", events.status.SUCCESS, post_files=[] - ) - ], - report_finish.call_args_list, - ) + assert report_finish.call_args_list == [ + mock.call( + "myname", "mymessage", events.status.SUCCESS, post_files=[] + ) + ] @mock.patch("cloudinit.reporting.events.report_finish_event") def test_message_updatable(self, report_finish): with events.ReportEventStack("myname", "mydesc") as c: c.message = "all good" - self.assertEqual( - [ - mock.call( - "myname", "all good", events.status.SUCCESS, post_files=[] - ) - ], - report_finish.call_args_list, - ) + assert report_finish.call_args_list == [ + mock.call( + "myname", "all good", events.status.SUCCESS, post_files=[] + ) + ] @mock.patch("cloudinit.reporting.events.report_start_event") @mock.patch("cloudinit.reporting.events.report_finish_event") @@ -425,8 +392,8 @@ def test_reporting_disabled_does_not_report_events( ): with events.ReportEventStack("a", "b", reporting_enabled=False): pass - self.assertEqual(report_start.call_count, 0) - self.assertEqual(report_finish.call_count, 0) + assert report_start.call_count == 0 + assert report_finish.call_count == 0 @mock.patch("cloudinit.reporting.events.report_start_event") @mock.patch("cloudinit.reporting.events.report_finish_event") @@ -440,25 +407,27 @@ def test_reporting_child_default_to_parent( with parent: with child: pass - self.assertEqual(report_start.call_count, 0) - self.assertEqual(report_finish.call_count, 0) + assert report_start.call_count == 0 + assert report_finish.call_count == 0 def test_reporting_event_has_sane_repr(self): myrep = events.ReportEventStack( "fooname", "foodesc", reporting_enabled=True ).__repr__() - self.assertIn("fooname", myrep) - self.assertIn("foodesc", myrep) - self.assertIn("True", myrep) + assert "fooname" in myrep + assert "foodesc" in myrep + assert "True" in myrep def test_set_invalid_result_raises_value_error(self): f = events.ReportEventStack("myname", "mydesc") - self.assertRaises(ValueError, setattr, f, "result", "BOGUS") + with pytest.raises(ValueError): + f.result = "BOGUS" -class TestStatusAccess(TestCase): +class TestStatusAccess: def test_invalid_status_access_raises_value_error(self): - self.assertRaises(AttributeError, getattr, events.status, "BOGUS") + with pytest.raises(AttributeError): + getattr(events.status, "BOGUS") @skipUnlessJsonSchema() From 254d459cd61feaa669325f1acfd8df04a94c0eae Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Tue, 2 Sep 2025 08:16:58 -0600 Subject: [PATCH 122/150] fix(test): marks applied to fixtures have no effect --- tests/unittests/runs/test_merge_run.py | 1 - tests/unittests/runs/test_simple_run.py | 1 - tests/unittests/sources/test_smartos.py | 1 - 3 files changed, 3 deletions(-) diff --git a/tests/unittests/runs/test_merge_run.py b/tests/unittests/runs/test_merge_run.py index 876f8c69db5..e0e46740d1e 100644 --- a/tests/unittests/runs/test_merge_run.py +++ b/tests/unittests/runs/test_merge_run.py @@ -11,7 +11,6 @@ from tests.unittests.helpers import replicate_test_root -@pytest.mark.usefixtures("fake_filesystem_hook") @pytest.fixture(autouse=True) def user_data(tmp_path): replicate_test_root("simple_ubuntu", str(tmp_path)) diff --git a/tests/unittests/runs/test_simple_run.py b/tests/unittests/runs/test_simple_run.py index 93493e72499..1a4ab1101e6 100644 --- a/tests/unittests/runs/test_simple_run.py +++ b/tests/unittests/runs/test_simple_run.py @@ -12,7 +12,6 @@ from tests.unittests.helpers import replicate_test_root -@pytest.mark.usefixtures("fake_filesystem_hook") @pytest.fixture(autouse=True) def replicate_root(tmp_path): replicate_test_root("simple_ubuntu", str(tmp_path)) diff --git a/tests/unittests/sources/test_smartos.py b/tests/unittests/sources/test_smartos.py index 8083df4825c..dc58e0ac5d2 100644 --- a/tests/unittests/sources/test_smartos.py +++ b/tests/unittests/sources/test_smartos.py @@ -1430,7 +1430,6 @@ def test_ipv6_addrconf(self): assert expected == found -@pytest.mark.allow_subp_for("mdata-get") @pytest.fixture def mdata_proc(): mdata_proc = multiprocessing.Process(target=start_mdata_loop) From ae2311d1e0225c71a4c9e488aea86a6bb797e785 Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Tue, 2 Sep 2025 08:25:29 -0600 Subject: [PATCH 123/150] fix(test): raises match against an empty string will always pass --- tests/unittests/config/test_cc_runcmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/config/test_cc_runcmd.py b/tests/unittests/config/test_cc_runcmd.py index bdf394a5122..622ac207f80 100644 --- a/tests/unittests/config/test_cc_runcmd.py +++ b/tests/unittests/config/test_cc_runcmd.py @@ -91,7 +91,7 @@ class TestRunCmdSchema: {"a": "n"}, ] }, - "", + "is not of type", ), ), ) From 2cdc58f24da74935796a80736fad2d9d87825ac1 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 3 Sep 2025 12:34:39 -0600 Subject: [PATCH 124/150] fix: bddeb tool needs distro debian param for package dependency lookup Use debian distro package renames and lookups when determining system package names for test-requirements.txt. Otherwise, pyserial dependency gets translated to invalid system package name python3-pyserial. --- packages/bddeb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/bddeb b/packages/bddeb index 162a58cb0c0..4e06378c773 100755 --- a/packages/bddeb +++ b/packages/bddeb @@ -102,7 +102,13 @@ def write_debian_folder(root, templ_data, cloud_util_deps): reqs = reqs_output.splitlines() test_reqs = run_helper( "read-dependencies", - ["--requirements-file", "test-requirements.txt", "--system-pkg-names"], + [ + "--distro", + "debian", + "--requirements-file", + "test-requirements.txt", + "--system-pkg-names", + ], ).splitlines() requires = ["cloud-utils | cloud-guest-utils"] if cloud_util_deps else [] From 1955952be1bf0fca8f9baa361a7c5a2615d093b9 Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Wed, 3 Sep 2025 15:14:30 -0600 Subject: [PATCH 125/150] fix(systemd): revert auditd.service dependency (#6448) Reverts cc8d1b4c RHBZ: 2390898 --- systemd/cloud-init-local.service.tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl index b8a2f33111c..e88b15ca246 100644 --- a/systemd/cloud-init-local.service.tmpl +++ b/systemd/cloud-init-local.service.tmpl @@ -7,7 +7,6 @@ DefaultDependencies=no {% endif %} Wants=network-pre.target After=hv_kvp_daemon.service -Before=auditd.service Before=network-pre.target Before=shutdown.target {% if variant in ["almalinux", "cloudlinux", "rhel"] %} From b12c9f1bb86b799d0478cef59dc40341234b2b6b Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Fri, 5 Sep 2025 12:27:59 -0600 Subject: [PATCH 126/150] docs(nocloud): Fix file schema examples (#6455) Fixes GH-5843 --- doc/rtd/reference/datasources/nocloud.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/rtd/reference/datasources/nocloud.rst b/doc/rtd/reference/datasources/nocloud.rst index 03e7654ccf6..9a1e9eb30c3 100644 --- a/doc/rtd/reference/datasources/nocloud.rst +++ b/doc/rtd/reference/datasources/nocloud.rst @@ -150,7 +150,7 @@ filesystem path for those that require more flexibility. This may be done with a line configuration: :: - ds=nocloud;s=file://path/to/directory/ + ds=nocloud;s=file:///path/to/directory/ Or a system configuration: @@ -158,7 +158,7 @@ Or a system configuration: datasource: NoCloud: - seedfrom: file://path/to/directory + seedfrom: file:///path/to/directory Source 2: Drive with labeled filesystem --------------------------------------- From 1e9aba6331a48d2404639876eb75c04601a00da9 Mon Sep 17 00:00:00 2001 From: 3405691582 Date: Fri, 5 Sep 2025 15:46:13 -0400 Subject: [PATCH 127/150] Optionalize bash completion. (#6441) To allow for a low-dependency installation of cloud-init, optionalize the bash-completion dependency, defaulted to true. This means behaviorally nothing changes, but that the bash-completion features can be easily turned off if not desired. --- meson.build | 22 ++++++++++++---------- meson_options.txt | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index 16a855bdfa8..6dc157d540f 100644 --- a/meson.build +++ b/meson.build @@ -28,16 +28,18 @@ install_subdir( ) # Binaries and script entrypoints -bash_completions_dir = dependency('bash-completion').get_variable( - pkgconfig: 'completionsdir', - default_value: get_option('datadir') / 'bash-completion' / 'completions', -) -install_data( - 'bash_completion/cloud-init', - install_dir: bash_completions_dir, - install_mode: 'rw-r--r--', - install_tag: 'bin', -) +if get_option('bash_completion') + bash_completions_dir = dependency('bash-completion').get_variable( + pkgconfig: 'completionsdir', + default_value: get_option('datadir') / 'bash-completion' / 'completions', + ) + install_data( + 'bash_completion/cloud-init', + install_dir: bash_completions_dir, + install_mode: 'rw-r--r--', + install_tag: 'bin', + ) +endif install_data( [ diff --git a/meson_options.txt b/meson_options.txt index f75f408778a..13de1f8cd39 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option('init_system', type: 'string', value: 'systemd', description: 'Set target init system.') option('distro_templates', type: 'array', value: [], description: 'Distro template files to install. WARNING: Templates may change in the future. If using this option, be sure to check new releases for template file changes.') option('disable_sshd_keygen', type: 'boolean', value: false, description: 'Provide systemd service to disable sshd-keygen if present to avoid races with cloud-init.') +option('bash_completion', type: 'boolean', value: true, description: 'Bash completion for cloud-init.') From 7b591b9bc7618be425dd90dabbee26acf1d636f0 Mon Sep 17 00:00:00 2001 From: Amirhossein Shaerpour <87924605+shaerpour@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:43:30 +0330 Subject: [PATCH 128/150] fix: properly quote commands in `do_as` function (#6400) Previously, the do_as function failed because commands were not quoted correctly. The quoting logic has been fixed to ensure commands execute reliably. Fixes GH-6171 --- cloudinit/distros/__init__.py | 5 ++++- tests/unittests/distros/test__init__.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index eb58095cb71..2c9daff2e75 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -13,6 +13,7 @@ import logging import os import re +import shlex import stat import string import urllib.parse @@ -1419,7 +1420,9 @@ def do_as(self, command: list, user: str, cwd: str = "", **kwargs): "-", user, "-c", - directory + "env PATH=$PATH " + " ".join(command), + directory + + "env PATH=$PATH " + + " ".join(shlex.quote(arg) for arg in command), ], **kwargs, ) diff --git a/tests/unittests/distros/test__init__.py b/tests/unittests/distros/test__init__.py index 2447d313e96..2769efc609a 100644 --- a/tests/unittests/distros/test__init__.py +++ b/tests/unittests/distros/test__init__.py @@ -325,6 +325,28 @@ def test_expire_passwd_freebsd_uses_pw_command(self): ["pw", "usermod", "myuser", "-p", "01-Jan-1970"] ) + @mock.patch(M_PATH + "subp.subp") + def test_do_as_nested_py_cmd(self, m_subp): + cls = distros.fetch("ubuntu") + d = cls("ubuntu", {}, None) + d.do_as( + command=[ + "/usr/bin/python3", + "-c", + "import site; print(site.getuserbase())", + ], + user="ubuntu", + ) + m_subp.assert_called_once_with( + [ + "su", + "-", + "ubuntu", + "-c", + "env PATH=$PATH /usr/bin/python3 -c 'import site; print(site.getuserbase())'", # noqa: E501 + ] + ) + class TestGetPackageMirrors: def return_first(self, mlist): From 8a90aa5abd315be6ed585fe25216c598c0a6ac29 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 10 Sep 2025 15:15:05 +0200 Subject: [PATCH 129/150] fix(bddeb): require exec permissions on temporary folder Fixes: GH-6141 --- packages/bddeb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bddeb b/packages/bddeb index 4e06378c773..2fbb1cca3ca 100755 --- a/packages/bddeb +++ b/packages/bddeb @@ -306,7 +306,7 @@ def main(): "release_suffix": get_release_suffix(args.release), } - with temp_utils.tempdir() as tdir: + with temp_utils.tempdir(needs_exe=True) as tdir: # output like 0.7.6-1022-g36e92d3 ver_data = read_version() From f68c11dcc8a3bbe35fb90f4958730e495c892788 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 10 Sep 2025 17:41:12 +0200 Subject: [PATCH 130/150] fix(net-convert): respect output directory with netplan Respect --directory when --output-kind is netplan. Previously, if the netplan python API was present, cloud-init was unconditionally ignoring the directory target and writing to /etc/netplan/50-cloud-init.yaml or a similar location using the netplan API. That writing operation only succeeded if the user had write permission under /etc/netplan. In the no-permission case, cloud-init falls back to manually writing to $target + etc/netplan/50-cloud-init.yaml and that's why tests didn't catch this issue. Fixes: GH-6141 --- cloudinit/net/netplan.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 96341a5a506..cf874756428 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -243,7 +243,9 @@ def _clean_default(target=None): os.unlink(f) -def netplan_api_write_yaml_file(net_config_content: str) -> bool: +def netplan_api_write_yaml_file( + net_config_content: str, target: Optional[str] = None +) -> bool: """Use netplan.State._write_yaml_file to write netplan config Where netplan python API exists, prefer to use of the private @@ -281,9 +283,11 @@ def netplan_api_write_yaml_file(net_config_content: str) -> bool: # determine default root-dir /etc/netplan and/or specialized # filenames or read permissions based on whether this config # contains secrets. - state_output_file._write_yaml_file( - os.path.basename(CLOUDINIT_NETPLAN_FILE) - ) + if not target: + file = os.path.basename(CLOUDINIT_NETPLAN_FILE) + else: + file = target + state_output_file._write_yaml_file(file) except Exception as e: LOG.warning( "Unable to render network config using netplan python module." @@ -392,7 +396,7 @@ def render_network_state( content = header + content netplan_config_changed = has_netplan_config_changed(fpnplan, content) - if not netplan_api_write_yaml_file(content): + if not netplan_api_write_yaml_file(content, target=fpnplan): fallback_write_netplan_yaml(fpnplan, content) if self.clean_default: From d4f268a5115e22a5b719d7d2d3165ffe10b14a18 Mon Sep 17 00:00:00 2001 From: Philipp Bender Date: Thu, 11 Sep 2025 10:07:13 +0200 Subject: [PATCH 131/150] feat(hetzner): enable hotplug support and prepare IPv6 integration (#6445) This PR tries to accomplish hotplug support for the Hetzner cloud. cloud-init should pick up the newly attached private network and configure the interface accordingly. Furthermore, this PR also prepares cloud-init to use the IPv6 Hetzner metadata-service --- cloudinit/sources/DataSourceHetzner.py | 150 +++++++++++++++---- cloudinit/sources/helpers/hetzner.py | 49 ++++-- doc/module-docs/cc_install_hotplug/data.yaml | 2 +- tests/unittests/sources/test_hetzner.py | 69 ++++++--- 4 files changed, 202 insertions(+), 68 deletions(-) diff --git a/cloudinit/sources/DataSourceHetzner.py b/cloudinit/sources/DataSourceHetzner.py index 53a950f8e94..2aaba2a5435 100644 --- a/cloudinit/sources/DataSourceHetzner.py +++ b/cloudinit/sources/DataSourceHetzner.py @@ -9,81 +9,149 @@ import logging import cloudinit.sources.helpers.hetzner as hc_helper -from cloudinit import dmi, net, sources, util +from cloudinit import dmi, net, sources, url_helper, util +from cloudinit.event import EventScope, EventType from cloudinit.net.dhcp import NoDHCPLeaseError -from cloudinit.net.ephemeral import EphemeralDHCPv4 +from cloudinit.net.ephemeral import EphemeralIPNetwork LOG = logging.getLogger(__name__) -BASE_URL_V1 = "http://169.254.169.254/hetzner/v1" - BUILTIN_DS_CONFIG = { - "metadata_url": BASE_URL_V1 + "/metadata", - "userdata_url": BASE_URL_V1 + "/userdata", + "metadata_path": "metadata", + "metadata_private_networks_path": "metadata/private-networks", + "userdata_path": "userdata", } MD_RETRIES = 60 MD_TIMEOUT = 2 MD_WAIT_RETRY = 2 +MD_MAX_WAIT = 120 +MD_SLEEP_TIME = 2 +# Do not re-configure the network on non-Hetzner network interface +# changes. Currently, Hetzner private network addresses start with 0x86. +EXTRA_HOTPLUG_UDEV_RULES = """ +SUBSYSTEM=="net", ATTR{address}=="86:*", GOTO="cloudinit_hook" +GOTO="cloudinit_end" +""" -class DataSourceHetzner(sources.DataSource): +def base_urls_v1(): + return ( + f"http://[fe80::a9fe:a9fe%25{net.find_fallback_nic()}]/hetzner/v1/", + "http://169.254.169.254/hetzner/v1/", + ) + + +class DataSourceHetzner(sources.DataSource): dsname = "Hetzner" + default_update_events = { + EventScope.NETWORK: { + EventType.BOOT_NEW_INSTANCE, + EventType.BOOT, + EventType.HOTPLUG, + } + } + def __init__(self, sys_cfg, distro, paths): sources.DataSource.__init__(self, sys_cfg, distro, paths) self.distro = distro - self.metadata = dict() + self.metadata = {} self.ds_cfg = util.mergemanydict( [ util.get_cfg_by_path(sys_cfg, ["datasource", "Hetzner"], {}), BUILTIN_DS_CONFIG, ] ) - self.metadata_address = self.ds_cfg["metadata_url"] - self.userdata_address = self.ds_cfg["userdata_url"] + self.metadata_path = self.ds_cfg["metadata_path"] + self.metadata_private_networks_path = self.ds_cfg[ + "metadata_private_networks_path" + ] + self.userdata_path = self.ds_cfg["userdata_path"] self.retries = self.ds_cfg.get("retries", MD_RETRIES) self.timeout = self.ds_cfg.get("timeout", MD_TIMEOUT) self.wait_retry = self.ds_cfg.get("wait_retry", MD_WAIT_RETRY) + self.max_wait = self.ds_cfg.get("max_wait", MD_MAX_WAIT) + self.sleep_time = self.ds_cfg.get("sleep_time", MD_SLEEP_TIME) self._network_config = sources.UNSET self.dsmode = sources.DSMODE_NETWORK self.metadata_full = None + self.extra_hotplug_udev_rules = EXTRA_HOTPLUG_UDEV_RULES + + def _unpickle(self, ci_pkl_version: int) -> None: + super()._unpickle(ci_pkl_version) + self.extra_hotplug_udev_rules = EXTRA_HOTPLUG_UDEV_RULES + self.wait_retry = self.ds_cfg.get("wait_retry", MD_WAIT_RETRY) + self.max_wait = self.ds_cfg.get("max_wait", MD_MAX_WAIT) + self.sleep_time = self.ds_cfg.get("sleep_time", MD_SLEEP_TIME) + self.metadata_path = self.ds_cfg["metadata_path"] + self.metadata_private_networks_path = self.ds_cfg[ + "metadata_private_networks_path" + ] + self.userdata_path = self.ds_cfg["userdata_path"] + def _get_data(self): (on_hetzner, serial) = get_hcloud_data() if not on_hetzner: return False + base_urls = base_urls_v1() try: - with EphemeralDHCPv4( + with EphemeralIPNetwork( self.distro, - iface=net.find_fallback_nic(), + interface=net.find_fallback_nic(), + ipv4=True, + ipv6=True, connectivity_urls_data=[ { - "url": BASE_URL_V1 + "/metadata/instance-id", + "url": url_helper.combine_url( + url, f"{self.metadata_path}/instance-id" + ) } + for url in base_urls ], ): - md = hc_helper.read_metadata( - self.metadata_address, + url, contents = hc_helper.get_metadata( + [ + url_helper.combine_url(url, self.metadata_path) + for url in base_urls + ], + max_wait=self.max_wait, timeout=self.timeout, - sec_between=self.wait_retry, - retries=self.retries, + sleep_time=self.sleep_time, ) - ud = hc_helper.read_userdata( - self.userdata_address, + LOG.debug("Using metadata source: '%s'", url) + md = util.load_yaml(contents.decode(), allowed=(dict, list)) + url, contents = hc_helper.get_metadata( + [ + url_helper.combine_url( + url, self.metadata_private_networks_path + ) + for url in base_urls + ], + max_wait=self.max_wait, timeout=self.timeout, - sec_between=self.wait_retry, - retries=self.retries, + sleep_time=self.sleep_time, ) - pn = hc_helper.read_metadata( - self.metadata_address + "/private-networks", + LOG.debug("Using private_networks source: '%s'", url) + md["private-networks"] = util.load_yaml( + contents.decode(), allowed=(dict, list) + ) + url, ud = hc_helper.get_metadata( + [ + url_helper.combine_url(url, self.userdata_path) + for url in base_urls + ], + max_wait=self.max_wait, timeout=self.timeout, - sec_between=self.wait_retry, - retries=self.retries, + sleep_time=self.sleep_time, ) + LOG.debug("Using userdata source: '%s'", url) + if not ud: + LOG.debug("Got empty userdata") except NoDHCPLeaseError as e: LOG.error("Bailing, DHCP Exception: %s", e) raise @@ -105,7 +173,7 @@ def _get_data(self): self.metadata["local-hostname"] = md["hostname"] self.metadata["network-config"] = md.get("network-config", None) self.metadata["public-keys"] = md.get("public-keys", None) - self.metadata["private-networks"] = pn + self.metadata["private-networks"] = md.get("private-networks", []) self.vendordata_raw = md.get("vendor_data", None) # instance-id and serial from SMBIOS should be identical @@ -138,19 +206,37 @@ def network_config(self): if self._network_config != sources.UNSET: return self._network_config - _net_config = self.metadata["network-config"] - if not _net_config: + net_config = self.metadata["network-config"] + if not net_config: raise RuntimeError("Unable to get meta-data from server....") - self._network_config = _net_config - + private_networks = self.metadata.get("private-networks", []) + private_networks_config = [] + for private_network in private_networks: + private_networks_config.append( + { + "type": "physical", + "mac_address": private_network["mac_address"], + "name": hc_helper.get_interface_name_from_mac( + private_network["mac_address"] + ), + "subnets": [ + { + "ipv4": True, + "type": "dhcp", + } + ], + } + ) + net_config["config"].extend(private_networks_config) + self._network_config = net_config return self._network_config def get_hcloud_data(): vendor_name = dmi.read_dmi_data("system-manufacturer") if vendor_name != "Hetzner": - return (False, None) + return False, None serial = dmi.read_dmi_data("system-serial-number") if serial: @@ -158,7 +244,7 @@ def get_hcloud_data(): else: raise RuntimeError("Hetzner Cloud detected, but no serial found") - return (True, serial) + return True, serial # Used to match classes to dependencies diff --git a/cloudinit/sources/helpers/hetzner.py b/cloudinit/sources/helpers/hetzner.py index 50fbcb0468a..68abd4dc48d 100644 --- a/cloudinit/sources/helpers/hetzner.py +++ b/cloudinit/sources/helpers/hetzner.py @@ -3,22 +3,41 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import url_helper, util +from typing import Optional, Tuple +from cloudinit import net, url_helper -def read_metadata(url, timeout=2, sec_between=2, retries=30): - response = url_helper.readurl( - url, timeout=timeout, sec_between=sec_between, retries=retries - ) - if not response.ok(): - raise RuntimeError("unable to read metadata at %s" % url) - return util.load_yaml(response.contents.decode(), allowed=(dict, list)) +def _skip_retry_on_empty_response(cause: url_helper.UrlError) -> bool: + return cause.code != 204 -def read_userdata(url, timeout=2, sec_between=2, retries=30): - response = url_helper.readurl( - url, timeout=timeout, sec_between=sec_between, retries=retries - ) - if not response.ok(): - raise RuntimeError("unable to read userdata at %s" % url) - return response.contents + +def get_metadata( + urls, + max_wait=120, + timeout=2, + sleep_time=2, +) -> Tuple[Optional[str], bytes]: + try: + url, contents = url_helper.wait_for_url( + urls=urls, + max_wait=max_wait, + timeout=timeout, + sleep_time=sleep_time, + # It is ok for userdata to not exist (that's why we are stopping if + # HTTP code is 204) and just in that case returning an empty + # string. + exception_cb=_skip_retry_on_empty_response, + ) + if not url: + raise RuntimeError("No data received from urls: '%s':" % urls) + return url, contents + except url_helper.UrlError as e: + if e.code == 204: + return e.url, b"" + raise + + +def get_interface_name_from_mac(mac: str) -> Optional[str]: + mac_to_iface = net.get_interfaces_by_mac() + return mac_to_iface.get(mac.lower()) diff --git a/doc/module-docs/cc_install_hotplug/data.yaml b/doc/module-docs/cc_install_hotplug/data.yaml index 2277c9f274a..abdac913a46 100644 --- a/doc/module-docs/cc_install_hotplug/data.yaml +++ b/doc/module-docs/cc_install_hotplug/data.yaml @@ -14,7 +14,7 @@ cc_install_hotplug: around this limitation, one can wait until cloud-init has completed before hotplugging devices. - Currently supported datasources: Openstack, EC2 + Currently supported datasources: Openstack, EC2, Hetzner examples: - comment: | Example 1: Enable hotplug of network devices diff --git a/tests/unittests/sources/test_hetzner.py b/tests/unittests/sources/test_hetzner.py index 268837562db..af0a893cd0f 100644 --- a/tests/unittests/sources/test_hetzner.py +++ b/tests/unittests/sources/test_hetzner.py @@ -10,8 +10,7 @@ from cloudinit.sources import DataSourceHetzner from tests.unittests.helpers import mock -METADATA = util.load_yaml( - """ +METADATA = b""" hostname: cloudinit-test instance-id: 123456 local-ipv4: '' @@ -45,13 +44,24 @@ test-key@workstation vendor_data: "test" """ -) USERDATA = b"""#cloud-config runcmd: - [touch, /root/cloud-init-worked ] """ +PRIVATE_NETWORKS = b""" +- ip: 10.1.0.2 + alias_ips: [] + interface_num: 2 + mac_address: 86:00:00:aa:5d:f8 + network_id: 11352901 + network_name: network-2 + network: 10.1.0.0/16 + subnet: 10.1.0.0/24 + gateway: 10.1.0.1 +""" + class TestDataSourceHetzner: """ @@ -68,16 +78,14 @@ def ds(self, paths, tmp_path): return ds @mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery") - @mock.patch("cloudinit.sources.DataSourceHetzner.EphemeralDHCPv4") + @mock.patch("cloudinit.sources.DataSourceHetzner.EphemeralIPNetwork") @mock.patch("cloudinit.net.find_fallback_nic") - @mock.patch("cloudinit.sources.helpers.hetzner.read_metadata") - @mock.patch("cloudinit.sources.helpers.hetzner.read_userdata") + @mock.patch("cloudinit.sources.helpers.hetzner.get_metadata") @mock.patch("cloudinit.sources.DataSourceHetzner.get_hcloud_data") def test_read_data( self, m_get_hcloud_data, - m_usermd, - m_readmd, + m_get_metadata, m_fallback_nic, m_net, m_dhcp, @@ -85,10 +93,15 @@ def test_read_data( ): m_get_hcloud_data.return_value = ( True, - str(METADATA.get("instance-id")), + str(util.load_yaml(METADATA).get("instance-id")), ) - m_readmd.return_value = METADATA.copy() - m_usermd.return_value = USERDATA + # Use side_effect to return values for the three sequential calls to + # helpers.hetzner.get_metadata: metadata, private-networks, userdata + m_get_metadata.side_effect = [ + ("metadata_url", METADATA), + ("privnets_url", PRIVATE_NETWORKS), + ("userdata_url", USERDATA), + ] m_fallback_nic.return_value = "eth0" m_dhcp.return_value = [ { @@ -104,29 +117,45 @@ def test_read_data( m_net.assert_called_once_with( ds.distro, - iface="eth0", + interface="eth0", + ipv4=True, + ipv6=True, connectivity_urls_data=[ + { + "url": "http://[fe80::a9fe:a9fe%25eth0]/hetzner/v1/metadata/instance-id" + }, { "url": "http://169.254.169.254/hetzner/v1/metadata/instance-id" - } + }, ], ) - assert 0 != m_readmd.call_count + assert 0 != m_get_metadata.call_count - assert METADATA.get("hostname") == ds.get_hostname().hostname + assert ( + util.load_yaml(METADATA).get("hostname") + == ds.get_hostname().hostname + ) - assert METADATA.get("public-keys") == ds.get_public_ssh_keys() + assert ( + util.load_yaml(METADATA).get("public-keys") + == ds.get_public_ssh_keys() + ) + assert ds.metadata["private-networks"] == util.load_yaml( + PRIVATE_NETWORKS, allowed=(dict, list) + ) assert isinstance(ds.get_public_ssh_keys(), list) assert ds.get_userdata_raw() == USERDATA - assert ds.get_vendordata_raw() == METADATA.get("vendor_data") + assert ds.get_vendordata_raw() == util.load_yaml(METADATA).get( + "vendor_data" + ) - @mock.patch("cloudinit.sources.helpers.hetzner.read_metadata") + @mock.patch("cloudinit.sources.helpers.hetzner.get_metadata") @mock.patch("cloudinit.net.find_fallback_nic") @mock.patch("cloudinit.sources.DataSourceHetzner.get_hcloud_data") def test_not_on_hetzner_returns_false( - self, m_get_hcloud_data, m_find_fallback, m_read_md, ds + self, m_get_hcloud_data, m_find_fallback, m_get_metadata, ds ): """If helper 'get_hcloud_data' returns False, return False from get_data.""" @@ -136,4 +165,4 @@ def test_not_on_hetzner_returns_false( assert not ret # These are a white box attempt to ensure it did not search. assert 0 == m_find_fallback.call_count - assert 0 == m_read_md.call_count + assert 0 == m_get_metadata.call_count From f626daa0faca844927d001f0dd99d00f09d2ecfe Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Fri, 12 Sep 2025 12:22:47 +0200 Subject: [PATCH 132/150] fix: correct netplan renderer target (#6465) Only pass absolute path to netplan python API if an explicit target is passed to netplan.Renderer.render_network_state. That is, when called from cloud-init net-convert. --- cloudinit/net/netplan.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index cf874756428..98530d8422b 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -395,8 +395,14 @@ def render_network_state( header += "\n" content = header + content + # Customize target only if explicitly passed in + if target is None: + target_ = target + else: + target_ = fpnplan + netplan_config_changed = has_netplan_config_changed(fpnplan, content) - if not netplan_api_write_yaml_file(content, target=fpnplan): + if not netplan_api_write_yaml_file(content, target=target_): fallback_write_netplan_yaml(fpnplan, content) if self.clean_default: From b04c475f3147fd849bac6b3d41e1302b222c8406 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab <46541787+mostafaCamel@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:17:57 +0300 Subject: [PATCH 133/150] feat(schema): add support for the not keyword (#6469) In jsonschema v2.0.0 the not keyword was introduced. Provide support for this schema inversion in is_valid(). Fixes GH-6468 Signed-off-by: Mostafa Abdelwahab --- cloudinit/config/schema.py | 3 +- tests/unittests/config/test_schema.py | 53 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py index 08e956b66d6..6be53a3ba4c 100644 --- a/cloudinit/config/schema.py +++ b/cloudinit/config/schema.py @@ -500,11 +500,12 @@ def is_valid(self, instance, _schema=None, **__): It does ignore instances of `SchemaDeprecationError`. """ + validator = self if _schema is None else self.evolve(schema=_schema) errors = filter( lambda e: not isinstance( # pylint: disable=W1116 e, SchemaDeprecationError ), - self.evolve(schema=_schema).iter_errors(instance), + validator.iter_errors(instance), ) return next(errors, None) is None diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py index d158761e5e3..f0dd7a5c05b 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -524,6 +524,59 @@ def test_validateconfig_schema_honors_formats(self): str(context_mgr.value) ) + @skipUnlessJsonSchema() + @pytest.mark.parametrize( + "schema,should_succeed_validating,expected_err_msg_of_either_schema_or_its_negation", + [ + ( + {"required": ["a", "b"]}, + True, + "Cloud config schema errors: : ({'a': 5, 'b': 6} should not" + r" be valid under {'required': \['a', 'b'\]}|{'required': " + r"\['a', 'b'\]} is not allowed for {'a': 5, 'b': 6})", + ), + ( + {"required": ["a", "c"]}, + False, + "Cloud config schema errors: : 'c' is a required property", + ), + ( + {"required": ["d", "c"]}, + False, + "Cloud config schema errors: : 'c' is a required property, :" + " 'd' is a required property", + ), + ], + ) + def test_validateconfig_with_not_keyword_in_schema( + self, + schema, + should_succeed_validating, + expected_err_msg_of_either_schema_or_its_negation, + ): + """ + Test the behavior of the not keyword in a schema + """ + cfg_to_test = {"a": 5, "b": 6} + not_schema = {"not": schema} + (schema_to_succeed, schema_to_fail) = ( + (schema, not_schema) + if should_succeed_validating + else (not_schema, schema) + ) + + validate_cloudconfig_schema( + cfg_to_test, schema_to_succeed, strict=True + ) + + with pytest.raises( + SchemaValidationError, + match=expected_err_msg_of_either_schema_or_its_negation, + ): + validate_cloudconfig_schema( + cfg_to_test, schema_to_fail, strict=True + ) + @skipUnlessJsonSchema() def test_validateconfig_schema_honors_formats_strict_metaschema(self): """With strict and strict_metaschema True, ensure errors on format""" From 0a1aa7f1ee1ee92f2595c837e348595ac73c2c56 Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab Date: Mon, 11 Aug 2025 05:36:49 +0300 Subject: [PATCH 134/150] feat: new workflow to shellcheck debian package scripts Signed-off-by: Mostafa Abdelwahab --- .../workflows/shellcheck-debian-scripts.yml | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/shellcheck-debian-scripts.yml diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml new file mode 100644 index 00000000000..e04b9f366da --- /dev/null +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -0,0 +1,117 @@ +name: ShellCheck Debian package scripts + +env: + # This is a space separated string for multiple globs + # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` + # Assumption in this workflow: the resolved filepaths do not contain spaces. + GLOBS_TO_SHELLCHECK: "**/*.postinst **/*.postrm **/*.preinst **/*.prerm" + +# on.(pull_request|push).paths keys are not used because the steps in the jobs +# already handle filtering the file paths for specific globs. +# Also, we need the action to always be triggered (and succeed) if we decide to have this +# check to be blocking the PR. Otherwise (if on.(pull_request|push).paths are defined and the check is +# blocking the PR), the PRs which do not have changes in these specific files will be blocked. +# Another minor reason for not using on.(pull_request|push).paths : it does not accept environment +# variables so we would need to hardcode the yaml array to match env.GLOBS_TO_SHELLCHECK at the top of the file. +on: + pull_request: + # There is a known bug in GithUb but it will most probably not affect out use case + # https://github.com/orgs/community/discussions/118623#discussioncomment-9087833 + # When there are 2 PRs using the same source branch (actually the same head SHA to be more specific), with the base branch in one PR matching + # on.pull_request.branches and thee base branch in the second PR not matching this key, + # then the second PR will show these checks that were triggered by the first PR but not the second PR. + branches: + - 'ubuntu/**' + push: + branches: + - 'ubuntu/**' + +concurrency: + group: 'ci-${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + +defaults: + run: + # Using bash instead of the usual sh in the other workflows in order to support the globstar ** + shell: bash -ex {0} + +# Note: No need to specify the shell option in the shellcheck command. +# These linted scripts are for ubuntu so the !/bin/sh (i.e. system shell) shebang at the top of these scripts . +# So no need to pass the shell option to the shellcheck command as shellcheck will read the shebang at the top of the file (sh) so will +# use sh +jobs: + shellcheck-on-all-matching-files: + name: ShellCheck on all matching files in the repo + runs-on: ubuntu-24.04 + continue-on-error: true + steps: + - name: Repository checkout + uses: actions/checkout@v4 + + - name: Run ShellCheck + run: | + shopt -s globstar + echo "The globs to check are ${GLOBS_TO_SHELLCHECK}" + ALL_MATCHING_SCRIPTS=$(git ls-files ${GLOBS_TO_SHELLCHECK} | tr '\n' ' ') + echo "The matching scripts to shellcheck are ${ALL_MATCHING_SCRIPTS}" + if [ -z "${ALL_MATCHING_SCRIPTS}" ] + then + echo "There are no files in the repo which match the glob patterns so shellcheck will not run" + else + shellcheck ${ALL_MATCHING_SCRIPTS} + fi + + shellcheck-on-matching-and-changed-files: + name: ShellCheck on matching files that have changed + runs-on: ubuntu-24.04 + continue-on-error: true + steps: + - name: Repository checkout + uses: actions/checkout@v4 + + - name: Get all matching changed files + id: matching-changed-files + uses: tj-actions/changed-files@v46 + with: + files: ${{ env.GLOBS_TO_SHELLCHECK }} + files_separator: " " + + - name: Run shellcheck on the matching changed files + env: + ALL_CHANGED_FILES: ${{ steps.matching-changed-files.outputs.all_changed_and_modified_files }} + run: | + shopt -s globstar + if [ -z "${ALL_CHANGED_FILES}" ] + then + echo "There are no changed files in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" + else + echo "The changed and modified (including deleted) files are ${ALL_CHANGED_FILES}" + echo "The globs used are ${GLOBS_TO_SHELLCHECK}" + MATCHING_CHANGED_FILES=$(git ls-files ${ALL_CHANGED_FILES} | tr '\n' ' ') #filter out deleted files + echo "The matching and changed files are ${MATCHING_CHANGED_FILES}" + if [ -z "${MATCHING_CHANGED_FILES}" ] + then + echo "There are no changed files in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" + else + shellcheck ${MATCHING_CHANGED_FILES} + fi + fi + + differential-shellcheck: + name: Run Differential shellcheck + runs-on: ubuntu-24.04 + + permissions: + security-events: write + + steps: + - name: Repository checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Differential ShellCheck + uses: redhat-plumbers-in-action/differential-shellcheck@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + include-path: ${{ env.GLOBS_TO_SHELLCHECK }} \ No newline at end of file From a407a949932e50f90bac362cd2ab351bde47af3f Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab Date: Tue, 16 Sep 2025 08:55:48 +0300 Subject: [PATCH 135/150] remove the unretained jobs by the reviewers Signed-off-by: Mostafa Abdelwahab --- .../workflows/shellcheck-debian-scripts.yml | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index e04b9f366da..0b05cd6278d 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -40,27 +40,6 @@ defaults: # So no need to pass the shell option to the shellcheck command as shellcheck will read the shebang at the top of the file (sh) so will # use sh jobs: - shellcheck-on-all-matching-files: - name: ShellCheck on all matching files in the repo - runs-on: ubuntu-24.04 - continue-on-error: true - steps: - - name: Repository checkout - uses: actions/checkout@v4 - - - name: Run ShellCheck - run: | - shopt -s globstar - echo "The globs to check are ${GLOBS_TO_SHELLCHECK}" - ALL_MATCHING_SCRIPTS=$(git ls-files ${GLOBS_TO_SHELLCHECK} | tr '\n' ' ') - echo "The matching scripts to shellcheck are ${ALL_MATCHING_SCRIPTS}" - if [ -z "${ALL_MATCHING_SCRIPTS}" ] - then - echo "There are no files in the repo which match the glob patterns so shellcheck will not run" - else - shellcheck ${ALL_MATCHING_SCRIPTS} - fi - shellcheck-on-matching-and-changed-files: name: ShellCheck on matching files that have changed runs-on: ubuntu-24.04 @@ -96,22 +75,3 @@ jobs: shellcheck ${MATCHING_CHANGED_FILES} fi fi - - differential-shellcheck: - name: Run Differential shellcheck - runs-on: ubuntu-24.04 - - permissions: - security-events: write - - steps: - - name: Repository checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Differential ShellCheck - uses: redhat-plumbers-in-action/differential-shellcheck@v5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - include-path: ${{ env.GLOBS_TO_SHELLCHECK }} \ No newline at end of file From 0e5c06f40959ff549dbee99b47e2e07d95bbcc5e Mon Sep 17 00:00:00 2001 From: Mostafa Abdelwahab Date: Tue, 16 Sep 2025 09:06:13 +0300 Subject: [PATCH 136/150] Address review Signed-off-by: Mostafa Abdelwahab --- .../workflows/shellcheck-debian-scripts.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index 0b05cd6278d..e6b34619cfe 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -6,16 +6,9 @@ env: # Assumption in this workflow: the resolved filepaths do not contain spaces. GLOBS_TO_SHELLCHECK: "**/*.postinst **/*.postrm **/*.preinst **/*.prerm" -# on.(pull_request|push).paths keys are not used because the steps in the jobs -# already handle filtering the file paths for specific globs. -# Also, we need the action to always be triggered (and succeed) if we decide to have this -# check to be blocking the PR. Otherwise (if on.(pull_request|push).paths are defined and the check is -# blocking the PR), the PRs which do not have changes in these specific files will be blocked. -# Another minor reason for not using on.(pull_request|push).paths : it does not accept environment -# variables so we would need to hardcode the yaml array to match env.GLOBS_TO_SHELLCHECK at the top of the file. on: pull_request: - # There is a known bug in GithUb but it will most probably not affect out use case + # There is a known bug in Github but it will most probably not affect out use case # https://github.com/orgs/community/discussions/118623#discussioncomment-9087833 # When there are 2 PRs using the same source branch (actually the same head SHA to be more specific), with the base branch in one PR matching # on.pull_request.branches and thee base branch in the second PR not matching this key, @@ -35,22 +28,20 @@ defaults: # Using bash instead of the usual sh in the other workflows in order to support the globstar ** shell: bash -ex {0} -# Note: No need to specify the shell option in the shellcheck command. -# These linted scripts are for ubuntu so the !/bin/sh (i.e. system shell) shebang at the top of these scripts . -# So no need to pass the shell option to the shellcheck command as shellcheck will read the shebang at the top of the file (sh) so will -# use sh +# Note: No need to specify the shell option in the shellcheck command +# as shellcheck reads and uses the shebang at the top of the linted scripts. jobs: shellcheck-on-matching-and-changed-files: name: ShellCheck on matching files that have changed runs-on: ubuntu-24.04 - continue-on-error: true steps: - name: Repository checkout uses: actions/checkout@v4 - name: Get all matching changed files id: matching-changed-files - uses: tj-actions/changed-files@v46 + # make sure to use a SHA not a version + uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 with: files: ${{ env.GLOBS_TO_SHELLCHECK }} files_separator: " " From e84d994cee9daeafff3b85f70c841de8346448bf Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 19 Sep 2025 14:56:06 -0600 Subject: [PATCH 137/150] specific patchw matches drop globstar --- .../workflows/shellcheck-debian-scripts.yml | 64 +- debian/NEWS | 29 + debian/README.source | 30 + debian/apport-general-hook.py | 12 + debian/apport-launcher.py | 7 + debian/changelog | 6998 +++++++++++++++++ debian/cherry-pick | 197 + debian/clean | 2 + debian/cloud-init-base.config | 99 + debian/cloud-init-base.lintian-overrides | 6 + debian/cloud-init-base.logrotate | 11 + debian/cloud-init-base.maintscript | 10 + debian/cloud-init-base.manpages | 3 + debian/cloud-init-base.postinst | 329 + debian/cloud-init-base.postrm | 15 + debian/cloud-init-base.preinst | 239 + debian/cloud-init-base.prerm | 6 + debian/cloud-init-base.templates | 21 + debian/control | 114 + debian/copyright | 40 + debian/gbp.conf | 12 + debian/gbp_format_changelog | 85 + debian/po/POTFILES.in | 1 + debian/po/templates.pot | 246 + debian/rules | 28 + debian/source/format | 1 + debian/source/lintian-overrides | 2 + debian/upstream/metadata | 5 + debian/watch | 2 + 29 files changed, 8602 insertions(+), 12 deletions(-) create mode 100644 debian/NEWS create mode 100644 debian/README.source create mode 100644 debian/apport-general-hook.py create mode 100644 debian/apport-launcher.py create mode 100644 debian/changelog create mode 100755 debian/cherry-pick create mode 100644 debian/clean create mode 100644 debian/cloud-init-base.config create mode 100644 debian/cloud-init-base.lintian-overrides create mode 100644 debian/cloud-init-base.logrotate create mode 100644 debian/cloud-init-base.maintscript create mode 100644 debian/cloud-init-base.manpages create mode 100644 debian/cloud-init-base.postinst create mode 100644 debian/cloud-init-base.postrm create mode 100644 debian/cloud-init-base.preinst create mode 100644 debian/cloud-init-base.prerm create mode 100644 debian/cloud-init-base.templates create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/gbp.conf create mode 100755 debian/gbp_format_changelog create mode 100644 debian/po/POTFILES.in create mode 100644 debian/po/templates.pot create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/source/lintian-overrides create mode 100644 debian/upstream/metadata create mode 100644 debian/watch diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index e6b34619cfe..e815e02a657 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -1,10 +1,10 @@ -name: ShellCheck Debian package scripts +name: ShellCheck Debian package scripts, error on new lints env: # This is a space separated string for multiple globs # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` # Assumption in this workflow: the resolved filepaths do not contain spaces. - GLOBS_TO_SHELLCHECK: "**/*.postinst **/*.postrm **/*.preinst **/*.prerm" + GLOBS_TO_SHELLCHECK: "debian/cherry-pick debian/*.config debian/*.postinst debian/*.postrm debian/*.preinst debian/*.prerm packages/debian/*.postrm" on: pull_request: @@ -25,8 +25,7 @@ concurrency: defaults: run: - # Using bash instead of the usual sh in the other workflows in order to support the globstar ** - shell: bash -ex {0} + shell: bash -e {0} # Note: No need to specify the shell option in the shellcheck command # as shellcheck reads and uses the shebang at the top of the linted scripts. @@ -50,19 +49,60 @@ jobs: env: ALL_CHANGED_FILES: ${{ steps.matching-changed-files.outputs.all_changed_and_modified_files }} run: | - shopt -s globstar if [ -z "${ALL_CHANGED_FILES}" ] then echo "There are no changed files in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" else echo "The changed and modified (including deleted) files are ${ALL_CHANGED_FILES}" echo "The globs used are ${GLOBS_TO_SHELLCHECK}" - MATCHING_CHANGED_FILES=$(git ls-files ${ALL_CHANGED_FILES} | tr '\n' ' ') #filter out deleted files - echo "The matching and changed files are ${MATCHING_CHANGED_FILES}" - if [ -z "${MATCHING_CHANGED_FILES}" ] - then - echo "There are no changed files in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" - else - shellcheck ${MATCHING_CHANGED_FILES} + + # Define high-water marks for expected lint info and warn levels + declare -A INFO_LIMITS WARN_LIMITS + INFO_LIMITS["debian/cherry-pick"]=5 + WARN_LIMITS["debian/cherry-pick"]=4 + INFO_LIMITS["debian/cloud-init-base.config"]=3 + WARN_LIMITS["debian/cloud-init-base.config"]=7 + INFO_LIMITS["debian/cloud-init-base.postinst"]=16 + WARN_LIMITS["debian/cloud-init-base.postinst"]=17 + INFO_LIMITS["debian/cloud-init-base.preinst"]=15 + WARN_LIMITS["debian/cloud-init-base.preinst"]=21 + + ALL_NEW_LINTS="" + for script_name in ${ALL_CHANGED_FILES}; do + NEW_SCRIPT_LINTS="" + if [ -f ${script_name} ]; then + # Only error when lint counts are greater than limits + info_limit=0 + warn_limit=0 + # Respect and defined limits, otherwise exit non-zero + if [[ -v INFO_LIMITS[$script_name] ]]; then + info_limit=${INFO_LIMITS[$script_name]} + fi + if [[ -v WARN_LIMITS[$script_name] ]]; then + warn_limit=${WARN_LIMITS[$script_name]} + fi + shellcheck ${script_name} > output.txt || true + lint_info_count=$( grep -c '(info)' output.txt ) || true + lint_warn_count=$( grep -c '(warning)' output.txt ) || true + lint_error_count=$( grep -c '(error)' output.txt ) || true + if [ $lint_error_count -gt 0 ]; then + NEW_SCRIPT_LINTS+="shellcheck $script_name contains errors.\n" + fi + if [ $lint_info_count -gt $info_limit ]; then + NEW_SCRIPT_LINTS+="shellcheck $script_name- too many (info) lints: $lint_info_count\n" + fi + if [ $lint_warn_count -gt $warn_limit ]; then + NEW_SCRIPT_LINTS+="shellcheck $script_name- too many (warning) lints: $lint_warn_count\n" + fi + fi + if [ -n "$NEW_SCRIPT_LINTS" ]; then + ALL_NEW_LINTS+=$NEW_SCRIPT_LINTS + ALL_NEW_LINTS+=$(cat output.txt) + fi + done + if [ -n "${ALL_NEW_LINTS}" ]; then + echo -e "${ALL_NEW_LINTS}" + exit 1 fi fi + echo "SUCCESS: no new shellcheck lints encountered" diff --git a/debian/NEWS b/debian/NEWS new file mode 100644 index 00000000000..93426a69356 --- /dev/null +++ b/debian/NEWS @@ -0,0 +1,29 @@ +cloud-init (24.4-0ubuntu1) plucky; urgency=medium + + Cloud-init added cloud-specific metapackages to better express + cloud-specific dependencies and reduce image size. + + The original cloud-init is still available as a metapackage and a smaller + version (called cloud-init-base) has been created to include just the + essential dependencies. + + A few specific metapackages have been created, while more cloud-specific + ones are under development. The current set include: + + * cloud-init-base - package that contains the cloud-init code along with + the minimal set of dependencies + * cloud-init - is a metapackage that retains every previous dependency + * cloud-init-cloud-sigma and cloud-init-smart-os - metapackages with an + additional dependency on python3-serial. They can be installed if the + target platform is CloudSigma or SmartOS + + If you are an image builder: + + * Install ``cloud-init``, containing all dependencies, if the target + platform is unknown + * Install ``cloud-init-base`` if the target platform(s) are known and do + not require specific dependencies + * Install a cloud-specific metapackage if the target platform has some + specific dependency requirements. + + -- Alberto Contreras Tue, 17 Sep 2024 11:02:35 +0200 diff --git a/debian/README.source b/debian/README.source new file mode 100644 index 00000000000..3db12bd8e42 --- /dev/null +++ b/debian/README.source @@ -0,0 +1,30 @@ +=== General Info === +Packaging branches for the Ubuntu development release are maintained in +upstream git under ubuntu/devel branch. + + git clone git://git.launchpad.net/cloud-init -b ubuntu/devel + +Stable release branches are also available in ubuntu/ + $ git clone git://git.launchpad.net/cloud-init + $ cd cloud-init + $ git branch -r + origin/HEAD -> origin/master + origin/master + origin/ubuntu/devel + origin/ubuntu/precise + origin/ubuntu/trusty + origin/ubuntu/xenial + +Patches in debian/patches are stored un-applied. + +== New snapshot == +Upstream maintains a script to handle new upstream snapshots at: + https://github.com/cloud-init/qa-scripts/ +Its usage is like: + new-upstream-snapshot master + +== Cherry Pick == +To cherry pick an upstream commit: + ./debian/cherry-pick + +That will add a patch to debian/patches/ and debian/patches/series. diff --git a/debian/apport-general-hook.py b/debian/apport-general-hook.py new file mode 100644 index 00000000000..5f614baa33b --- /dev/null +++ b/debian/apport-general-hook.py @@ -0,0 +1,12 @@ +"""General Apport hook for all reports that are using cloud-init.""" + +from cloudinit.apport import general_add_info + + +def add_info(report, ui) -> None: + """Entry point for Apport. + + Add a subset of non-sensitive cloud-init data from + /run/cloud/instance-data.json that will be helpful for debugging. + """ + general_add_info(report, ui) diff --git a/debian/apport-launcher.py b/debian/apport-launcher.py new file mode 100644 index 00000000000..9fce9997558 --- /dev/null +++ b/debian/apport-launcher.py @@ -0,0 +1,7 @@ +"""Wrapper for cloudinit apport interface""" + +from cloudinit.apport import add_info as cloudinit_add_info + + +def add_info(report, ui): + return cloudinit_add_info(report, ui) diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000000..b23048bbc65 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6998 @@ +cloud-init (25.3~2g890873f5-0ubuntu2) questing; urgency=medium + + * d/control: add all missing python3 package runtime deps (LP: #2121482) + + -- Chad Smith Wed, 27 Aug 2025 15:15:40 -0600 + +cloud-init (25.3~2g890873f5-0ubuntu1) questing; urgency=medium + + * d/control: add jinja2 and jsonpatch dependencies (LP: #2121482) + + -- Brett Holman Wed, 27 Aug 2025 09:01:05 -0600 + +cloud-init (25.3~1g890873f5-0ubuntu1) questing; urgency=medium + + * d/control: + - ensure binary packages depend on same version of cloud-init-base. + - update build-dependencies to support meson instead of setuptools + (LP: #1978328) (GH-5027) + * d/rules: + - use meson build backend instead of pybuild setuptools + (LP: #1978328) (GH-5027) + * Upstream snapshot based on upstream/main at 890873f5. + - Bugs fixed in this snapshot: (LP: #1978328) + + -- Chad Smith Fri, 22 Aug 2025 22:07:56 -0600 + +cloud-init (25.2-0ubuntu1) questing; urgency=medium + + * Upstream snapshot based on 25.2. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/25.2/ChangeLog + + -- James Falcon Tue, 12 Aug 2025 14:11:35 -0500 + +cloud-init (25.2~5g009c4cfd-0ubuntu1) questing; urgency=medium + + * Upstream snapshot based on upstream/main at 009c4cfd. + - Bugs fixed in this snapshot: + + fix: disable cloud-init when non-x86 environments have no DMI-data + and no strict datasources detected (LP: #2069607) (CVE-2024-6174) + + -- Chad Smith Tue, 24 Jun 2025 14:58:26 -0600 + +cloud-init (25.2~4geb9c1239-0ubuntu2) questing; urgency=medium + + * d/changelog: correct switched CVE references in prior release changelog + entry for 25.2~4geb9c1239-0ubuntu1 + + -- Chad Smith Tue, 24 Jun 2025 10:11:34 -0600 + +cloud-init (25.2~4geb9c1239-0ubuntu1) questing; urgency=medium + + * d/cloud-init-base.postinst: move existing hotplug-cmd fifo to root-only + share dir (CVE-2024-11584) + * Upstream snapshot based on upstream/main at eb9c1239. + - Bugs fixed in this snapshot: (LP: #2069607) + - security: make hotplug socket only writable by root (CVE-2024-11584) + - security: make ds-identify behavior strict datasource discovery on + non-x86 platforms without DMI data (LP: #2069607) (CVE-2024-6174) + + -- Chad Smith Thu, 12 Jun 2025 13:24:29 -0600 + +cloud-init (25.2~3gf731cdf3-0ubuntu1) questing; urgency=medium + + * Upstream snapshot based on upstream/main at f731cdf3. + + -- Chad Smith Fri, 06 Jun 2025 14:05:17 -0600 + +cloud-init (25.2~2ge2bef781-0ubuntu1) questing; urgency=medium + + * Upstream snapshot based on upstream/main at e2bef781. + - Bugs fixed in this snapshot: (LP: #2100232) + + -- Chad Smith Mon, 12 May 2025 17:46:37 -0600 + +cloud-init (25.2~1g7a0265d3-0ubuntu1) questing; urgency=medium + + * d/control: add Azure metapackage + + New Azure binary package depending on cloud-init-base and + python3-passlib. + * d/cloud-init-base.preinst: avoid sed of /etc/fstab when absent + * Upstream snapshot based on upstream/main at 7a0265d3. + - Bugs fixed in this snapshot: (LP: #2106671) + + -- James Falcon Thu, 17 Apr 2025 13:27:07 -0500 + +cloud-init (25.1.1-0ubuntu2) plucky; urgency=medium + + * d/control: Fix how cloud-init-base overwrites cloud-init files. + (LP: #2092333) + * d/cloud-init-base.postinst: don't version-gate the migration from using + cloud-init-base debconf values over cloud-init. + + -- James Falcon Tue, 01 Apr 2025 13:33:57 -0500 + +cloud-init (25.1.1-0ubuntu1) plucky; urgency=medium + + * Upstream bug fix release based on upstream bugfix 25.1.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/25.1.1/ChangeLog + * d/patches: remove all cpick* quilt patches due to upstream snapshot + + -- James Falcon Mon, 24 Mar 2025 15:02:52 -0500 + +cloud-init (25.1-0ubuntu3) plucky; urgency=medium + + * cherry-pick fixes for MAAS traceback (LP: #2100963) + - cherry-pick c60771d8: test: pytestify test_url_helper.py + - cherry-pick 8810a2dc: test: Remove CiTestCase from + test_url_helper.py + - cherry-pick 582f16c1: test: add OauthUrlHelper tests + - cherry-pick 9311e066: fix: Update OauthUrlHelper to use readurl + exception_cb + + -- James Falcon Thu, 13 Mar 2025 15:47:53 -0500 + +cloud-init (25.1-0ubuntu2) plucky; urgency=medium + + * d/control: add python3-passlib dependency to cloud-init + metapackage. (LP: #2098988) + + -- James Falcon Tue, 25 Feb 2025 08:16:55 -0600 + +cloud-init (25.1-0ubuntu1) plucky; urgency=medium + + * Upstream snapshot based on 25.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/25.1/ChangeLog + + -- James Falcon Thu, 20 Feb 2025 14:32:56 -0600 + +cloud-init (25.1~3geb1965a4-0ubuntu1) plucky; urgency=medium + + * Upstream snapshot based on upstream/main at eb1965a4. + + -- James Falcon Wed, 05 Feb 2025 10:20:41 -0600 + +cloud-init (25.1~2g4ee65391-0ubuntu1) plucky; urgency=medium + + * d/cloud-init-base.templates: Move VMware before OVF + See GH-4030 and LP: #1992509 + * d/cloud-init-base.lintian-overrides: Update lintian + warnings to use /usr/lib rather than /lib + * Upstream snapshot based on upstream/main at 4ee65391. + - Bugs fixed in this snapshot: + (LP: #2094179, #2094208, #2094857, #2094858) + + -- James Falcon Tue, 14 Jan 2025 14:10:49 -0600 + +cloud-init (24.4-0ubuntu1) plucky; urgency=medium + + * debian: Rename cloud-init to cloud-init-base + + Add infrastructure to be able to express cloud-specific cloud-init + binary packages. + + Keep a cloud-init metapackage to install every possible dependency and + be able to operate on any platform + + Add `Breaks:` and `Replaces:` dependency with + cloud-init <= 24.4~2g2e4c39b7-0ubuntu1 + to adopt and own files from cloud-init in cloud-init-base. + * d/cloud-init-base.{config,postinst}: + + Handle migration of debconf values migrating cloud-init/* values to + cloud-init-base/*. + * d/control: add cloud-sigma and smart-os packages + + Add cloud-init-cloud-sigma and cloud-init-smart-os binary packages + depending on cloud-init-base and python3-serial. + + Removes python3-serial from cloud-init-base and add it to cloud-init. + * d/cloud-init-base.templates: + + Add warning about image portability to dpkg-reconfigure. + * d/NEWS: add news about cloud-specific packages + * d/po/templates.pot and cloud-init.templates: enable CloudCIX by default. + * d/cloud-init-base.{config,postinst}: + + Drop noop clean-up of cloud-init/* debconf values + * Upstream snapshot based on 24.4. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/24.4/ChangeLog + - Bugs fixed in this snapshot: (LP: #1992853) + + -- James Falcon Mon, 25 Nov 2024 10:42:33 -0600 + +cloud-init (24.4~3+really24.3.1-0ubuntu4) oracular; urgency=medium + + * Bug fix release (LP: #2081124): + d/p/cpick-hotplugd-systemd-ordering-fix.patch: fix systemd ordering cycle + issues with network cloud-init-hotplugd.socket, NetworkManager and + dbus.socket by adding DefaultDependencies=no to cloud-init-hotplugd.socket. + + -- Chad Smith Fri, 20 Sep 2024 15:31:13 -0600 + +cloud-init (24.4~3+really24.3.1-0ubuntu3) oracular; urgency=medium + + * Bug fix release (LP: #2080688): + d/p/cpick-nocloudnet-seedfrom-sysconfig-fix.patch fix NoCloudNet + detection when seedfrom config provided in /etc/cloud/cloud.cfg/*.cfg + + -- Chad Smith Fri, 13 Sep 2024 14:44:45 -0600 + +cloud-init (24.4~3+really24.3.1-0ubuntu2) oracular; urgency=medium + + * d/cloud-init.postinst: fix MAAS preseed provisioning by replacing + handle_preseed_maas and handle_preseed_local_cloud_config functions + which were removed in commit cf13fd126 (GH-5685) + + -- Chad Smith Tue, 10 Sep 2024 13:41:14 -0600 + +cloud-init (24.4~3+really24.3.1-0ubuntu1) oracular; urgency=medium + + * d/cloud-init.lintian-overrides: + + Remove alien disabled tag: + alien-tag package-supports-alternative-init-but-no-init.d-script + * Upstream snapshot based on 24.3.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/24.3.1/ChangeLog + + -- Chad Smith Wed, 04 Sep 2024 15:55:20 -0600 + +cloud-init (24.4~2g2e4c39b7-0ubuntu1) oracular; urgency=medium + + * Upstream snapshot based on upstream/main at 2e4c39b7. + + -- Chad Smith Thu, 15 Aug 2024 10:10:28 -0600 + +cloud-init (24.4~1gedd92b71-0ubuntu1) oracular; urgency=medium + + * Add netcat dependency + * Upstream snapshot based on edd92b71. + + -- Chad Smith Tue, 13 Aug 2024 06:38:07 -0600 + +cloud-init (24.3~1g6e4153b3-0ubuntu1) oracular; urgency=medium + + * Upstream snapshot based on upstream/main at 6e4153b3. + - Bugs fixed in this snapshot: (LP: #2065787) + + -- Alberto Contreras Fri, 19 Jul 2024 17:09:53 +0200 + +cloud-init (24.2~5g8c396a4b-0ubuntu1) oracular; urgency=medium + + * Upstream snapshot based on upstream/main at 8c396a4b. + + -- Chad Smith Wed, 26 Jun 2024 12:30:42 -0600 + +cloud-init (24.2~4g5f40426f-0ubuntu1) oracular; urgency=medium + + * Upstream snapshot based on upstream/main at 5f40426f. + - Bugs fixed in this snapshot: (LP: #2066985) + The fix for this bug in the previous release was only a + partial fix. + + -- James Falcon Wed, 05 Jun 2024 12:52:40 -0500 + +cloud-init (24.2~3ge68c8c74-0ubuntu1) oracular; urgency=medium + + * Upstream snapshot based on upstream/main at e68c8c74. + - Bugs fixed in this snapshot: (LP: #2066979, #2066985) + + -- James Falcon Fri, 24 May 2024 12:08:51 -0500 + +cloud-init (24.2~2g51c6569f-0ubuntu1) oracular; urgency=medium + + * Upstream snapshot based on upstream/main at 51c6569f. + - Bugs fixed in this snapshot: (LP: #2064132) + + -- Chad Smith Fri, 03 May 2024 15:34:25 -0600 + +cloud-init (24.2~1g370e680c-0ubuntu1) oracular; urgency=medium + + * d/control: Remove netifaces + * Upstream snapshot based on upstream/main at 370e680c. + - Bugs fixed in this snapshot: (LP: #2047796, #2064132) + + -- Chad Smith Fri, 03 May 2024 10:50:46 -0600 + +cloud-init (24.1.3-0ubuntu3) noble; urgency=medium + + * cherry-pick 516fad6d: fix(url_helper): fix TCP connection leak on + readurl() retries + + -- Brett Holman Fri, 05 Apr 2024 17:18:48 -0600 + +cloud-init (24.1.3-0ubuntu2) noble; urgency=medium + + * Upstream bug fix release based on 24.1.4. + + functional fixes in debian/patches: + - cpick-f74b61ea-ephemeral-dhcpcd-Set-dhcpcd-interface-down + When dhcpcd client sets up a device to discover IMDS, ensure + link is set back down to avoid interface rename failures (GH-5019) + - cpick-372e80f8-net-dhcp-bump-dhcpcd-timeout-to-300s-5127 + Extend default timeout of dhcpcd command to 300 seconds + - cpick-77771023-net-dhcp-raise-InvalidDHCPLeaseFileError-on-error + Raise any dhcpcd lease parse failures as InvalidDHCPLeaseFileError + (GH-5128) + - cpick-f6ac6ee8-fix-dhcpcd-Make-lease-parsing-more-robust-5129 + Avoid tracebacks on empty invalid lease content from dhcpcd + and raise InvalidDHCPLeaseFileError with full lease parsed when errors + occur. (GH-5128) + + test fixes in debian/patches: + - d/p/cpick-accdfe6a-refactor-Import-log-module-rather-than-functions-5074 + Avoid mock failures in unit tests by module-level import of log + - cpick-144782a8-test-Remove-side-effects-from-tests-5074 + + -- Chad Smith Wed, 03 Apr 2024 12:26:52 -0600 + +cloud-init (24.1.3-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on 24.1.3. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/24.1.3/ChangeLog + + -- James Falcon Wed, 27 Mar 2024 08:27:29 -0500 + +cloud-init (24.1.2-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on 24.1.2. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/24.1.2/ChangeLog + - Bugs fixed in this snapshot: (LP: #2057763) + + -- Chad Smith Thu, 21 Mar 2024 07:53:15 -0600 + +cloud-init (24.1.1-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on 24.1.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/24.1.1/ChangeLog + - Bugs fixed in this snapshot: (LP: #2056439, #2056460, #2055077) + (LP: #2056194) + + -- Brett Holman Mon, 11 Mar 2024 21:09:37 -0600 + +cloud-init (24.1-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on 24.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/24.1/ChangeLog + + -- Brett Holman Thu, 29 Feb 2024 19:57:05 -0700 + +cloud-init (24.1~7g54599148-0ubuntu1) noble; urgency=medium + + * d/cloud-init.templates: enable WSL datasource by default. + * Restore upstream ChangeLog without any merges. + We have been merging an upstream ChangeLog spelling difference + downstream for no good reason. + * Upstream snapshot based on upstream/main at 54599148. + - Bugs fixed in this snapshot: (LP: #2055081, #2055219) + + -- James Falcon Tue, 27 Feb 2024 17:42:55 -0600 + +cloud-init (24.1~6ga4140119-0ubuntu1) noble; urgency=medium + + * d/control: Replace isc-dhcp-client with dhcpcd-base + * d/p/do-not-block-user-login.patch: + - Remove patch, snapd no longer blocks ssh + * d/s/: Move apport hook to main branch + * d/cloud-init.postinst: change priority of hotplug rules. + Avoids LP #1946003 on upgraded systems. References: + [0] https://github.com/canonical/cloud-init/pull/4799 + [1] commit/b519d861aff8b44a0610c176cb34adcbe28df144 + * Upstream snapshot based on upstream/main at a4140119. + + -- Brett Holman Fri, 16 Feb 2024 08:18:39 -0500 + +cloud-init (24.1~5g1f6eddd5-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on upstream/main at 1f6eddd5. + - Bugs fixed in this snapshot: (LP: #2046483) + + -- Chad Smith Fri, 02 Feb 2024 14:39:53 -0700 + +cloud-init (24.1~4gd9677655-0ubuntu1) noble; urgency=medium + + * d/cloud-init.maintscript: remove /etc/cloud/clean.d/README + * d/cloud-init.logrotate: add logrotate config for cloud-init + * Upstream snapshot based on upstream/main at d9677655. + - Bugs fixed in this snapshot: (LP: #2051147) + + -- Alberto Contreras Wed, 24 Jan 2024 18:45:19 +0100 + +cloud-init (24.1~3gb729a4c4-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on upstream/main at b729a4c4. + + -- Chad Smith Tue, 09 Jan 2024 15:38:50 -0700 + +cloud-init (24.1~2gb6a1c635-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on upstream/main at b6a1c635. + - Bugs fixed in this snapshot: (LP: #2045086, #1951639) + + -- Alberto Contreras Thu, 04 Jan 2024 10:16:55 +0100 + +cloud-init (24.1~1g1003d438-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on upstream/main at 1003d438. + - Bugs fixed in this snapshot: (LP: #2040325) + + -- Alberto Contreras Fri, 15 Dec 2023 11:34:16 +0100 + +cloud-init (23.4-0ubuntu1) noble; urgency=medium + + * Upstream snapshot based on 23.4. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/23.4/ChangeLog + + -- Alberto Contreras Mon, 04 Dec 2023 18:51:25 +0100 + +cloud-init (23.4~3g0cb0b80f-0ubuntu1) noble; urgency=medium + + * d/p/do-not-block-user-login.patch: + - Revert behavior, allow user login after cloud-init stage (LP: #2039505) + * d/control: add python3-apt as Recommends to read APT config from apt_pkg + * drop the following cherry-picks now included: + + d/p/cpick-0d9f149a-Pytestify-apt-config-test-modules-4424 + + d/p/cpick-5023e9f9-Refactor-test_apt_source_v1.py-to-use-pytest-4427 + + d/p/cpick-e9cdd7e3-Install-gnupg-if-gpg-not-found-4431 + + d/p/cpick-015543d3-apt-install-software-properties-common-when-absent-but + * Upstream snapshot based on upstream/main at 0cb0b80f. + - Bugs fixed in this snapshot: (LP: #2034273, #2030729) + (LP: #2038945, #2039453) + + -- Chad Smith Wed, 08 Nov 2023 22:24:09 -0700 + +cloud-init (23.3.1-0ubuntu2) mantic; urgency=medium + + * New upstream bug fix release based on 23.3.1. + + Install missing dependencies in apt module if required for minimal images. + - d/p/cpick-0d9f149a-Pytestify-apt-config-test-modules-4424 + - d/p/cpick-5023e9f9-Refactor-test_apt_source_v1.py-to-use-pytest-4427 + - d/p/cpick-e9cdd7e3-Install-gnupg-if-gpg-not-found-4431 + - d/p/cpick-015543d3-apt-install-software-properties-common-when-absent-but + + -- Brett Holman Thu, 28 Sep 2023 12:00:23 -0500 + +cloud-init (23.3.1-0ubuntu1) mantic; urgency=medium + + * New upstream bug fix release based on 23.3.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/23.3.1/ChangeLog + - Bugs fixed in this snapshot: (LP: #2034273) + + -- Chad Smith Tue, 05 Sep 2023 17:03:59 -0600 + +cloud-init (23.3-0ubuntu1) mantic; urgency=medium + + * d/po/templates.pot: refresh with debconf-updatepo + * d/cloud-init.templates: enable Akamai by default. + Add Akamai to the default templates to allow datasource discovery. + * Upstream snapshot based on 23.3. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/23.3/ChangeLog + - Bugs fixed in this snapshot: (LP: #2030788, #1980150, #1884127) + (LP: #1998539) + + -- Chad Smith Mon, 28 Aug 2023 12:33:17 -0600 + +cloud-init (23.3~4ga31370af-0ubuntu1) mantic; urgency=medium + + * Upstream snapshot based on upstream/main at a31370af. + - Bugs fixed in this snapshot: (LP: #1923363, #2028562, #2028784) + * d/cloud-init.maintscript: Remove the unused hook-network-manager + conffile. (LP: #2027861) + * d/control: Add python3-passlib as needed for testing + + -- James Falcon Mon, 31 Jul 2023 11:38:42 -0500 + +cloud-init (23.3~3gebd7b2b6-0ubuntu1) mantic; urgency=medium + + * Upstream snapshot based on upstream/main at ebd7b2b6. + + -- Alberto Contreras Tue, 18 Jul 2023 17:37:07 +0200 + +cloud-init (23.3~2gcf474da7-0ubuntu1) mantic; urgency=medium + + * Upstream snapshot based on upstream/main at cf474da7. + - Bugs fixed in this snapshot: (LP: #2013403) + + -- Alberto Contreras Tue, 11 Jul 2023 10:22:04 +0200 + +cloud-init (23.3~1ge5a617fe-0ubuntu1) mantic; urgency=medium + + * Upstream snapshot based on upstream/main at e5a617fe. + - Bugs fixed in this snapshot: (LP: #2025180, #2025180, #1999952) + (LP: #1798055) + + -- Chad Smith Wed, 28 Jun 2023 09:57:55 -0600 + +cloud-init (23.2-0ubuntu1) mantic; urgency=medium + + * d/control: Remove pep8 dependency. It is no longer used. + * Upstream snapshot based on 23.2. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/23.2/ChangeLog + - Bugs fixed in this snapshot: (LP: #2011783, #2008888, #1854594, #2016350) + (LP: #1881925, #2013967, #2011291) + + -- Alberto Contreras Wed, 24 May 2023 19:06:03 +0200 + +cloud-init (23.1.1-0ubuntu2) lunar; urgency=medium + + * d/apport-general-hook.py: Add general apport hook to append cloud type, + image and instance size information to bug reports (LP: #1724623) + * d/cloud-init.postinst: MAAS prefer yaml.safe_load over yaml.load + (LP: #2009746) + * d/cloud-init.preinst: Oracle to remove vestigial /etc/cloud.cloud.cfg.d/ + 99-disable-network-config.cfg because system config is now honored before + datasource config (LP: #1956788) + * Upstream snapshot based on upstream/main at 85b2fbc2. + - Bugs fixed in this snapshot: (LP: #1956788) + + -- Chad Smith Tue, 21 Mar 2023 14:43:55 -0600 + +cloud-init (23.1.1-0ubuntu1) lunar; urgency=medium + + * Upstream snapshot based on main at 4896402b. + + -- James Falcon Thu, 02 Mar 2023 16:46:05 -0600 + +cloud-init (23.1-0ubuntu2) lunar; urgency=medium + + * Upstream snapshot based on main at d1ffbea5. + - Bugs fixed in this snapshot: (LP: #2008727) + + -- James Falcon Thu, 02 Mar 2023 14:05:48 -0600 + +cloud-init (23.1-0ubuntu1) lunar; urgency=medium + + * Upstream snapshot based on 23.1. + List of changes from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/23.1/ChangeLog + - Bugs fixed in this snapshot: (LP: #1990513, #1999164, #1931174) + (LP: #1815990) + + -- Alberto Contreras Wed, 22 Feb 2023 18:15:10 +0100 + +cloud-init (22.4.2-0ubuntu3) lunar; urgency=medium + + * Upstream snapshot based on upstream/main at bdc7b040. + - Bugs fixed in this snapshot: (LP: #2001737, #1998526, #2000596) + (LP: #1851438) + * Upstream snapshot based on upstream/main at 3b8b4692. + - Bugs fixed in this snapshot: (LP: #1998998, #1997922, #2002969) + (LP: #2003562, #1999680) + + -- James Falcon Fri, 03 Feb 2023 15:36:12 -0600 + +cloud-init (22.4.2-0ubuntu2) lunar; urgency=medium + + * Upstream snapshot based on upstream/main at 040090a3. + - Bugs fixed in this snapshot: (LP: #1994980, #1998655, #1999042) + (LP: #1997559) + + -- Chad Smith Thu, 15 Dec 2022 16:11:37 -0700 + +cloud-init (22.4.2-0ubuntu1) lunar; urgency=medium + + * Upstream snapshot based on upstream commit 893df0d6. + - Bugs fixed in this snapshot: (LP: #1997559) + + -- Alberto Contreras Thu, 24 Nov 2022 08:39:08 +0100 + +cloud-init (22.4.1-0ubuntu1) lunar; urgency=medium + + * Upstream snapshot based on upstream commit c038808e. + + -- James Falcon Mon, 21 Nov 2022 15:02:34 -0600 + +cloud-init (22.4-0ubuntu4) lunar; urgency=medium + + * Upstream snapshot based on upstream commit 74ed1387. + - Bugs fixed in this snapshot: (LP: #1844191, #1906849, #1992512) + + -- James Falcon Fri, 18 Nov 2022 16:41:49 -0600 + +cloud-init (22.4-0ubuntu3) lunar; urgency=medium + + * d/changelog: Sync released d/changelog entries + - 22.3.4-0ubuntu1 and 22.3.3-0ubuntu1~22.10.1 + * Changelog: Sync release entries in Changlog: + - 22.3.1, 22.3.2, 22.3.3 and 22.3.4 + + -- Chad Smith Tue, 15 Nov 2022 19:08:24 -0700 + +cloud-init (22.4-0ubuntu2) lunar; urgency=medium + + * d/changelog: account for closed bugs released in 22.4-0ubuntu1 + + -- Chad Smith Tue, 15 Nov 2022 16:34:07 -0700 + +cloud-init (22.4-0ubuntu1) lunar; urgency=medium + + * d/control: drop python3-httpretty from Build-Depends + * d/cloud-init.templates: Add NWCS to datasource list + * Upstream snapshot based on 22.4 upstream release. List of changes + from upstream can be found at + https://raw.githubusercontent.com/canonical/cloud-init/22.4/ChangeLog + * Bugs fixed in this snapshot: + (LP: #1962343, #1913461, #1883122, #1989686, #1307667, #1990070, #1991567) + (LP: #1991024, #1990041, #1993836) + + -- James Falcon Tue, 15 Nov 2022 14:23:44 -0600 + +cloud-init (22.3.4-0ubuntu1) kinetic; urgency=medium + + * New upstream bug fix release. + + Release 22.3.4 (LP: #1986703) + + Fix Oracle DS primary interface when using IMDS (#1757) + (LP: #1989686) + + -- Brett Holman Fri, 30 Sep 2022 16:07:44 -0600 + +cloud-init (22.3.3-0ubuntu1~22.10.1) kinetic; urgency=medium + + * New upstream bug fix release. + + Release 22.3.3 (LP: #1986703) + + Fix Oracle DS not setting subnet when using IMDS (#1735) + (LP: #1989686) + + azure: define new attribute for pre-22.3 pickles (#1725) + + sources/azure: ensure instance id is always correct (#1727) + [Chris Patterson] + + -- Brett Holman Mon, 19 Sep 2022 13:13:06 -0600 + +cloud-init (22.3-13-g70ce6442-0ubuntu1~22.10.1) kinetic; urgency=medium + + * New upstream snapshot. + + Fix v2 interface matching when no MAC (LP: #1986551) + + test: reduce number of network dependencies in flaky test (#1702) + + docs: publish cc_ubuntu_autoinstall docs to rtd (#1696) + + net: Fix EphemeraIPNetwork (#1697) + + test: make ansible test work across older versions (#1691) + + Networkd multi-address support/fix (#1685) [Teodor Garzdin] + + make: drop broken targets (#1688) + + net: Passthough v2 netconfigs in netplan systems (#1650) + (LP: #1978543) + + NM ipv6 connection does not work on Azure and Openstack (#1616) + [Emanuele Giuseppe Esposito] + + Fix check_format_tip (#1679) + + -- Brett Holman Tue, 30 Aug 2022 14:28:27 -0600 + +cloud-init (22.3-3-g9f0efc47-0ubuntu1~22.10.1) kinetic; urgency=medium + + * New upstream snapshot. + + DataSourceVMware: fix var use before init (#1674) (LP: #1987005) + [Andrew Kutz] + + rpm/copr: ensure RPM represents new clean.d dir artifacts (#1680) + + test: avoid centos leaked check of /etc/yum.repos.d/epel-testing.repo + (#1676) + + typo correction in Changelog + + -- Chad Smith Tue, 23 Aug 2022 14:30:10 -0600 + +cloud-init (22.3-0ubuntu1~22.10.1) kinetic; urgency=medium + + * New upstream release. + + Release 22.3 (#1662) (LP: #1986703) + + sources: obj.pkl cache should be written anyime get_data is run + (#1669) + + schema: drop release number from version file (#1664) + + pycloudlib: bump to quiet azure HTTP info logs (#1668) + + test: fix wireguard integration tests (#1666) + + Github is deprecating the 18.04 runner starting 12.1 (#1665) + + integration tests: Ensure one setup for all tests (#1661) + + tests: ansible test fixes (#1660) + + Prevent concurrency issue in test_webhook_hander.py (#1658) + + Workaround net_setup_link race with udev (#1655) (LP: #1983516) + + test: drop erroneous lxd assertion, verify command succeeded (#1657) + + Fix Chrony usage on Centos Stream (#1648) (LP: #1885952) + [Sven Haardiek] + + sources/azure: handle network unreachable errors for savable PPS + (#1642) [Chris Patterson] + + Return cc_set_hostname to PER_INSTANCE frequency (#1651) + (LP: #1983811) + + test: Collect integration test time by default (#1638) + + test: Drop forced package install hack in lxd integration test (#1649) + + schema: Resolve user-data if --system given (#1644) (LP: #1983306) + + -- Brett Holman Fri, 19 Aug 2022 10:49:08 -0600 + +cloud-init (22.2-139-gb64ba456-0ubuntu1~22.10.1) kinetic; urgency=medium + + * New upstream snapshot. + + test: use fake filesystem to avoid file removal (#1647) + + tox: Fix tip-flake8 and tip-mypy (#1635) + + config: Add wireguard config module (#1570) + [Fabian Lichtenegger-Lukas] + + tests: can run without azure-cli, tests expect inactive ansible + (#1643) + + typing: Type UrlResponse.contents (#1633) + + testing: fix references to `DEPRECATED.` (#1641) + + ssh_util: Handle sshd_config.d folder (LP: #1968873) + + schema: Enable deprecations in cc_update_etc_hosts (#1631) + + Add Ansible Config Module (#1579) + + util: Support Idle process state in get_proc_ppid() (#1637) + + schema: Enable deprecations in cc_growpart (#1628) + + schema: Enable deprecations in cc_users_groups (#1627) + + util: Fix error path and parsing in get_proc_ppid() + + main: avoid downloading full contents cmdline urls (#1606) + (LP: #1937319) + + schema: Enable deprecations in cc_scripts_vendor (#1629) + + schema: Enable deprecations in cc_set_passwords (#1630) + + sources/azure: add experimental support for preprovisioned os disks + (#1622) [Chris Patterson] + + Remove configobj a_to_u calls (#1632) [Stefano Rivera] + + cc_debug: Drop this module (#1614) + + schema: add aggregate descriptions in anyOf/oneOf (#1636) + + testing: migrate test_sshutil to pytest (#1617) + + testing: Fix test_ca_certs integration test (#1626) + + testing: add support for pycloudlib's pro images (#1604) + + testing: migrate test_cc_set_passwords to pytest (#1615) + + -- Alberto Contreras Wed, 10 Aug 2022 22:23:47 +0200 + +cloud-init (22.2-115-g6e498773-0ubuntu1~22.10.1) kinetic; urgency=medium + + * d/control: lintian fixes: + + Drop deprecated keys: XS-Python-Version and XB-Python-Version. + * d/cloud-init.lintian-overrides: lintian fixes: + + Fix systemd-service-file-refers-to-unusual-wantedby-target format. + * d/cloud-init.postinst: lintian fixes: + + Fix command-with-path-in-maintainer-script for grub-install. + * d/source/lintian-overrides: lintian fixes: + + silence binary-nmu-debian-revision-in-source bug: + https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1014584 + * d/gbp_format_changelog: add alberto as filtered contributor + * New upstream snapshot. + + network: add system_info network activator cloud.cfg overrides (#1619) + (LP: #1982857) + + lxd: lvm avoid thinpool when kernel module absent + + lxd: enable MTU configuration in cloud-init + + cc_set_passwords fixes (#1590) + + Modernise importer.py and type ModuleDetails (#1605) + + config: Def activate_by_schema_keys for t-z (#1613) + + config: define activate_by_schema_keys for p-r mods (#1611) + + clean: add param to remove /etc/machine-id for golden image creation + + config: define `activate_by_schema_keys` for a-f mods (#1608) + + config: define activate_by_schema_keys for s mods (#1612) + + config: Define activate_by_schema_keys for g-n mods (#1610) + + meta-schema: add infra to skip inapplicable modules + + sources/azure: don't set cfg["password"] for default user pw (#1592) + [Chris Patterson] + + schema: activate grub-dpkg deprecations (#1600) + + cc_lxd: Add btrfs and lvm lxd storage options (SC-1026) (#1585) + + cc_ubuntu_autoinstall: support live-installer autoinstall config + + clean: allow third party cleanup scripts in /etc/cloud/clean.d (#1581) + + sources/azure: refactor chassis asset tag handling (#1574) + [Chris Patterson] + + bsd: Don't assume that root user is in root group (#1587) + + Update govc command in VMWare walkthrough (#1576) [manioo8] + + sources/azure: increase read-timeout to 60 seconds for wireserver + (#1571) [Chris Patterson] + + Resource leak cleanup (#1556) + + Fix expire passwords for hashed passwords (#1577) (LP: #1979065) + [Sadegh Hayeri] + + mounts: fix suggested_swapsize for > 64GB hosts (#1569) + [Steven Stallion] + + Update chpasswd schema to deprecate password parsing (#1517) + + Schema deprecation handling (#1549) + + sources/azure: refactor ovf-env.xml parsing (#1550) [Chris Patterson] + + schema: Force stricter validation (#1547) + + ubuntu advantage config: http_proxy, https_proxy (#1512) + [Fabian Lichtenegger-Lukas] + + net: fix interface matching support (#1552) (LP: #1979877) + + -- Alberto Contreras Fri, 29 Jul 2022 18:51:43 +0200 + +cloud-init (22.2-64-g1fcd55d6-0ubuntu1~22.10.1) kinetic; urgency=medium + + * d/control: add python3-debconf as Depends and Build-Depends + * d/gbp.conf d/gbp_format_changelog: + + git-buildpackage customization for debian/changelog generation + * New upstream snapshot. + + tests: mock dns lookup that causes long timeouts (#1555) + + tox: add unpinned env for do_format and check_format (#1554) + + cc_ssh_import_id: Substitute deprecated warn (#1553) + [Alberto Contreras] + + Remove schema errors from log (#1551) (LP: #1978422) (CVE-2022-2084) + + Update WebHookHandler to run as background thread (SC-456) (#1491) + (LP: #1910552) + + testing: Don't run custom cloud dir test on Bionic (#1542) + + bash completion: update schema command (#1543) (LP: #1979547) + + CI: add non-blocking run against the linters tip versions (#1531) + [Paride Legovini] + + Change groups within the users schema to support lists and strings + (#1545) [RedKrieg] + + make it clear which username should go in the contributing doc (#1546) + + Pin setuptools for Travis (SC-1136) (#1540) + + Fix LXD datasource crawl when BOOT enabled (#1537) + + testing: Fix wrong path in dual stack test (#1538) + + cloud-config: honor cloud_dir setting (#1523) (LP: #1976564) + [Alberto Contreras] + + Add python3-debconf to pkg-deps.json Build-Depends (#1535) + [Alberto Contreras] + + redhat spec: udev/rules.d lives under /usr/lib on rhel-based systems + (#1536) + + tests/azure: add test coverage for DisableSshPasswordAuthentication + (#1534) [Chris Patterson] + + summary: Add david-caro to the cla signers (#1527) [David Caro] + + Add support for OpenMandriva (https://openmandriva.org/) (#1520) + [Bernhard Rosenkraenzer] + + tests/azure: refactor ovf creation (#1533) [Chris Patterson] + + Improve DataSourceOVF error reporting when script disabled (#1525) + [rong] + + tox: integration-tests-jenkins: softfail if only some test failed + (#1528) [Paride Legovini] + + CI: drop linters from Travis CI (moved to GH Actions) (#1530) + [Paride Legovini] + + sources/azure: remove unused encoding support for customdata (#1526) + [Chris Patterson] + + sources/azure: remove unused metadata captured when parsing ovf + (#1524) [Chris Patterson] + + sources/azure: remove dscfg parsing from ovf-env.xml (#1522) + [Chris Patterson] + + Remove extra space from ec2 dual stack crawl message (#1521) + + tests/azure: use namespaces in generated ovf-env.xml documents (#1519) + [Chris Patterson] + + setup.py: adjust udev/rules default path (#1513) + [Emanuele Giuseppe Esposito] + + Add python3-deconf dependency (#1506) [Alberto Contreras] + + Change match macadress param for network v2 config (#1518) + [Henrique Caricatti Capozzi] + + sources/azure: remove unused userdata property from ovf (#1516) + [Chris Patterson] + + sources/azure: minor refactoring to network config generation (#1497) + [Chris Patterson] + + net: Implement link-local ephemeral ipv6 + + Rename function to avoid confusion (#1501) + + Fix cc_phone_home requiring 'tries' (#1500) (LP: #1977952) + + datasources: replace networking functions with stdlib and + cloudinit.net code + + Remove xenial references (#1472) [Alberto Contreras] + + Oracle ds changes (#1474) (LP: #1967942) [Alberto Contreras] + + improve runcmd docs (#1498) + + add 3.11-dev to Travis CI (#1493) + + Only run github actions on pull request (#1496) + + Fix integration test client creation (#1494) [Alberto Contreras] + + tox: add link checker environment, fix links (#1480) + + cc_ubuntu_advantage: Fix doc (#1487) [Alberto Contreras] + + cc_yum_add_repo: Fix repo id canonicalization (#1489) (LP: #1975818) + [Alberto Contreras] + + Add linitio as contributor in the project (#1488) [Kevin Allioli] + + net-convert: use yaml.dump for debugging python NetworkState obj + (#1484) (LP: #1975907) + + test_schema: no relative $ref URLs, replace $ref with local path + (#1486) + + cc_set_hostname: do not write "localhost" when no hostname is given + (#1453) [Emanuele Giuseppe Esposito] + + Update .github-cla-signers (#1478) [rong] + + schema: write_files defaults, versions $ref full URL and add vscode + (#1479) + + docs: fix external links, add one more to the list (#1477) + + doc: Document how to change module frequency (#1481) + + tests: bump pycloudlib (#1482) + + tests: bump pycloudlib pinned commit for kinetic Azure (#1476) + + testing: fix test_status.py (#1475) + + integration tests: If KEEP_INSTANCE = True, log IP (#1473) + + Drop mypy excluded files (#1454) [Alberto Contreras] + + Docs additions (#1470) + + Add "formatting tests" to Github Actions + + Remove unused arguments in function signature (#1471) + + Changelog: correct errant classification of LP issues as GH (#1464) + + Use Network-Manager and Netplan as default renderers for RHEL and + Fedora (#1465) [Emanuele Giuseppe Esposito] + + -- James Falcon Fri, 01 Jul 2022 10:07:15 -0500 + +cloud-init (22.2-0ubuntu1~22.10.1) kinetic; urgency=medium + + * d/control: + - Build-Depends: add python3-responses and python3-pytest-mock for unittests + - Suggests: add openssh-server and ssh-import-id + * d/cloud-init.postinst: + - remove deprecated emit_upstart from cloud.cfg on upgrade + * New upstream release. + - Release 22.2 (LP: #1960939) + - Fix test due to caplog incompatibility (#1461) [Alberto Contreras] + - Align rhel custom files with upstream (#1431) + [Emanuele Giuseppe Esposito] + - cc_write_files: Improve schema. (#1460) [Alberto Contreras] + - cli: Redact files with permission errors in commands (#1440) + [Alberto Contreras] (LP: #1953430) + - Improve cc_set_passwords. (#1456) [Alberto Contreras] + - testing: make fake cloud-init wait actually wait (#1459) + - Scaleway: Fix network configuration for netplan 0.102 and later (#1455) + [Maxime Corbin] + - Fix 'ephmeral' typos in disk names(#1452) [Mike Hucka] + - schema: version schema-cloud-config-v1.json (#1424) + - cc_modules: set default meta frequency value when no config available + (#1457) + - Log generic warning on non-systemd systems. (#1450) [Alberto Contreras] + - cc_snap.maybe_install_squashfuse no longer needed in Bionic++. (#1448) + [Alberto Contreras] + - Drop support of *-sk keys in cc_ssh (#1451) [Alberto Contreras] + - testing: Fix console_log tests (#1437) + - tests: cc_set_passoword update for systemd, non-systemd distros (#1449) + - Fix bug in url_helper/dual_stack() logging (#1426) + - schema: render schema paths from _CustomSafeLoaderWithMarks (#1391) + - testing: Make integration tests kinetic friendly (#1441) + - Handle error if SSH service no present. (#1422) + [Alberto Contreras] (LP: #1969526) + - Fix network-manager activator availability and order (#1438) + - sources/azure: remove reprovisioning marker (#1414) [Chris Patterson] + - upstart: drop vestigial support for upstart (#1421) + - testing: Ensure NoCloud detected in test (#1439) + - Update .github-cla-signers kallioli [Kevin Allioli] + - Consistently strip top-level network key (#1417) (LP: #1906187) + - testing: Fix LXD VM metadata test (#1430) + - testing: Add NoCloud setup for NoCloud test (#1425) + - Update linters and adapt code for compatibility (#1434) [Paride Legovini] + - run-container: add support for LXD VMs (#1428) [Paride Legovini] + - integration-reqs: bump pycloudlib pinned commit (#1427) [Paride Legovini] + - Fix NoCloud docs (#1423) + - Docs fixes (#1406) + - docs: Add docs for module creation (#1415) + - Remove cheetah from templater (#1416) + - tests: verify_ordered_items fallback to re.escape if needed (#1420) + - Misc module cleanup (#1418) + - docs: Fix doc warnings and enable errors (#1419) + [Alberto Contreras] (LP: #1876341) + - Refactor cloudinit.sources.NetworkConfigSource to enum (#1413) + [Alberto Contreras] (LP: #1874875) + - Don't fail if IB and Ethernet devices 'collide' (#1411) + - Use cc_* module meta defintion over hardcoded vars (SC-888) (#1385) + - Fix cc_rsyslog.py initialization (#1404) [Alberto Contreras] + - Promote cloud-init schema from devel to top level subcommand (#1402) + - mypy: disable missing imports warning for httpretty (#1412) + [Chris Patterson] + - users: error when home should not be created AND ssh keys provided + [Jeffrey 'jf' Lim] + - Allow growpart to resize encrypted partitions (#1316) + - Fix typo in integration_test.rst (#1405) [Alberto Contreras] + - cloudinit.net refactor: apply_network_config_names (#1388) + [Alberto Contreras] (LP: #1884602) + - tests/azure: add fixtures for hardcoded paths (markers and data_dir) + (#1399) [Chris Patterson] + - testing: Add responses workaround for focal/impish (#1403) + - cc_ssh_import_id: fix is_key_in_nested_dict to avoid early False + - Fix ds-identify not detecting NoCloud seed in config (#1381) + (LP: #1876375) + - sources/azure: retry dhcp for failed processes (#1401) [Chris Patterson] + - Move notes about refactorization out of CONTRIBUTING.rst (#1389) + - Shave ~8ms off generator runtime (#1387) + - Fix provisioning dhcp timeout to 20 minutes (#1394) [Chris Patterson] + - schema: module example strict testing fix seed_random + - cc_set_hostname: examples small typo (perserve vs preserve) + [Wouter Schoot] + - sources/azure: refactor http_with_retries to remove **kwargs (#1392) + [Chris Patterson] + - declare dependency on ssh-import-id (#1334) + - drop references to old dependencies and old centos script + - sources/azure: only wait for primary nic to be attached during restore + (#1378) [Anh Vo] + - cc_ntp: migrated legacy schema to cloud-init-schema.json (#1384) + - Network functions refactor and bugfixes (#1383) + - schema: add JSON defs for modules cc_users_groups (#1379) (LP: #1858930) + - Fix doc typo (#1382) [Alberto Contreras] + - Add support for dual stack IPv6/IPv4 IMDS to Ec2 (#1160) + - Fix KeyError when rendering sysconfig IPv6 routes (#1380) (LP: #1958506) + - Return a namedtuple from subp() (#1376) + - Mypy stubs and other tox maintenance (SC-920) (#1374) + - Distro Compatibility Fixes (#1375) + - Pull in Gentoo patches (#1372) + - schema: add json defs for modules U-Z (#1360) + (LP: #1858928, #1858929, #1858931, #1858932) + - util: atomically update sym links to avoid Suppress FileNotFoundError + when reading status (#1298) [Adam Collard] (LP: LP:1962150) + - schema: add json defs for modules scripts-timezone (SC-801) (#1365) + - docs: Add first tutorial (SC-900) (#1368) + - BUG 1473527: module ssh-authkey-fingerprints fails Input/output error… + (#1340) [Andrew Lee] (LP: #1473527) + - add arch hosts template (#1371) + - ds-identify: detect LXD for VMs launched from host with > 5.10 kernel + (#1370) (LP: #1968085) + - Support EC2 tags in instance metadata (#1309) [Eduardo Dobay] + - schema: add json defs for modules e-install (SC-651) (#1366) + - Improve "(no_create_home|system): true" test (#1367) [Jeffrey 'jf' Lim] + - Expose https_proxy env variable to ssh-import-id cmd (#1333) + [Michael Rommel] + - sources/azure: remove bind/unbind logic for hot attached nic (#1332) + [Chris Patterson] + - tox: add types-* packages to check_format env (#1362) + - tests: python 3.10 is showing up in cloudimages (#1364) + - testing: add additional mocks to test_net tests (#1356) [yangzz-97] + - schema: add JSON schema for mcollective, migrator and mounts modules + (#1358) + - Honor system locale for RHEL (#1355) [Wei Shi] + - doc: Fix typo in cloud-config-run-cmds.txt example (#1359) [Ali Shirvani] + - ds-identify: also discover LXD by presence from DMI board_name = LXD + (#1311) + - black: bump pinned version to 22.3.0 to avoid click dependency issues + (#1357) + - Various doc fixes (#1330) + - testing: Add missing is_FreeBSD mock to networking test (#1353) + - Add --no-update to add-apt-repostory call (SC-880) (#1337) + - schema: add json defs for modules K-L (#1321) + (LP: #1858899, #1858900, #1858901, #1858902) + - docs: Re-order readthedocs install (#1354) + - Stop cc_ssh_authkey_fingerprints from ALWAYS creating home (#1343) + [Jeffrey 'jf' Lim] + - docs: add jinja2 pin (#1352) + - Vultr: Use find_candidate_nics, use ipv6 dns (#1344) [eb3095] + - sources/azure: move get_ip_from_lease_value out of shim (#1324) + [Chris Patterson] + - Fix cloud-init status --wait when no datasource found (#1349) + (LP: #1966085) + - schema: add JSON defs for modules resize-salt (SC-654) (#1341) + - Add myself as a future contributor (#1345) [Neal Gompa (ニール・ゴンパ)] + - Update .github-cla-signers (#1342) [Jeffrey 'jf' Lim] + - add Requires=cloud-init-hotplugd.socket in cloud-init-hotplugd.service + file (#1335) [yangzz-97] + - Fix sysconfig render when set-name is missing (#1327) + [Andrew Kutz] (LP: #1855945) + - Refactoring helper funcs out of NetworkState (#1336) [Andrew Kutz] + - url_helper: add tuple support for readurl timeout (#1328) + [Chris Patterson] + - Make fs labels match for ds-identify and docs (#1329) + - Work around bug in LXD VM detection (#1325) + - Remove redundant generator logs (#1318) + - tox: set verbose flags for integration tests (#1323) [Chris Patterson] + - net: introduce find_candidate_nics() (#1313) [Chris Patterson] + - Revert "Ensure system_cfg read before ds net config on Oracle (#1174)" + (#1326) + - Add vendor_data2 support for ConfigDrive source (#1307) [cvstealth] + - Make VMWare data source test host independent and expand testing (#1308) + [Robert Schweikert] + - Add json schemas for modules starting with P + - sources/azure: remove lease file parsing (#1302) [Chris Patterson] + - remove flaky test from ci (#1322) + - ci: Switch to python 3.10 in Travis CI (#1320) + - Better interface handling for Vultr, expect unexpected DHCP servers + (#1297) [eb3095] + - Remove unused init local artifact (#1315) + - Doc cleanups (#1317) + - docs improvements (#1312) + - add support for jinja do statements, add unit test (#1314) + [Paul Bruno] (LP: #1962759) + - sources/azure: prevent tight loops for DHCP retries (#1285) + [Chris Patterson] + - net/dhcp: surface type of DHCP lease failure to caller (#1276) + [Chris Patterson] + - Stop hardcoding systemctl location (#1278) [Robert Schweikert] + - Remove python2 syntax from docs (#1310) + - [tools/migrate-lp-user-to-github] Rename master branch to main (#1301) + [Adam Collard] + - redhat: Depend on "hostname" package (#1288) [Lubomir Rintel] + - Add native NetworkManager support (#1224) [Lubomir Rintel] + - Fix link in CLA check to point to contribution guide. (#1299) + [Adam Collard] + + -- Brett Holman Wed, 18 May 2022 11:23:42 -0600 + +cloud-init (22.1-14-g2e17a0d6-0ubuntu1~22.04.5) jammy; urgency=medium + + * d/p/cpick-be9389c6-Work-around-bug-in-LXD-VM-detection-1325: + cherry-pick be9389c6: Work around bug in LXD VM detection (#1325) + * d/p/cpick-30ccd51a-ds-identify-also-discover-LXD-by-presence-from-DMI: + cherry-pick 30ccd51a: ds-identify: also discover LXD by presence + from DMI + * d/p/pick-e3307e4d-ds-identify-detect-LXD-for-VMs-launched-from-host-with: + cherry-pick e3307e4d: ds-identify: detect LXD for VMs launched from + host with > 5.10 kernel (LP: #1968085) + + -- James Falcon Wed, 06 Apr 2022 16:48:16 -0500 + +cloud-init (22.1-14-g2e17a0d6-0ubuntu1~22.04.4) jammy; urgency=medium + + * d/p/cpick-eee60329-Fix-cloud-init-status-wait-when-no-datasource-found: + cherry-pick eee60329: Fix cloud-init status --wait when no datasource + found (#1349) + + -- Chad Smith Wed, 23 Mar 2022 20:10:41 -0600 + +cloud-init (22.1-14-g2e17a0d6-0ubuntu1~22.04.3) jammy; urgency=medium + + * d/patches: include the missing quilt patch from v. 22.04.2 upload + - cpick-5e347d25-Revert-Ensure-system_cfg-read-before-ds-net-config-on + + -- James Falcon Fri, 11 Mar 2022 15:19:52 -0600 + +cloud-init (22.1-14-g2e17a0d6-0ubuntu1~22.04.2) jammy; urgency=medium + + * cherry-pick 156b927e: Revert "Ensure system_cfg read before ds + net config on Oracle (#1174)" (#1326) + + -- James Falcon Fri, 11 Mar 2022 11:55:37 -0600 + +cloud-init (22.1-14-g2e17a0d6-0ubuntu1~22.04.1) jammy; urgency=medium + + * debian/apport-launcher.py: Fix string and whitespace formatting + * New upstream snapshot. + - check for existing symlink while force creating symlink (#1281) + [Shreenidhi Shedi] + - Do not silently ignore integer uid (#1280) (LP: #1875772) + - tests: create a IPv4/IPv6 VPC in Ec2 integration tests (#1291) + - Integration test fix ppa (#1296) + - tests: on official EC2. cloud-id actually startswith aws not ec2 (#1289) + - test_ppa_source: accept both http and https URLs (#1292) + [Paride Legovini] + - Fix apt test on azure + - add "lkundrak" as contributor [Lubomir Rintel] + - Holmanb/integration test fix ppa (#1287) + - Include missing subcommand in manpage (#1279) + - Clean up artifacts from pytest, packaging, release with make clean + (#1277) + - sources/azure: ensure retries on IMDS request failure (#1271) + [Chris Patterson] + - sources/azure: removed unused saveable PPS paths (#1268) + [Chris Patterson] + + -- Brett Holman Thu, 24 Feb 2022 16:40:00 -0700 + +cloud-init (22.1-1-gb3d9acdd-0ubuntu1~22.04.1) jammy; urgency=medium + + * New upstream snapshot. + - integration tests: fix Azure failures (#1269) + - Release 22.1 (#1267) (LP: #1960939) + - sources/azure: report ready in local phase (#1265) [Chris Patterson] + - sources/azure: validate IMDS network configuration metadata (#1257) + [Chris Patterson] + - docs: Add more details to runcmd docs (#1266) + - use PEP 589 syntax for TypeDict (#1253) + - mypy: introduce type checking (#1254) [Chris Patterson] + - Fix extra ipv6 issues, code reduction and simplification (#1243) [eb3095] + - tests: when generating crypted password, generate in target env (#1252) + - sources/azure: address mypy/pyright typing complaints (#1245) + [Chris Patterson] + - Docs for x-shellscript* userdata (#1260) + - test_apt_security: azure platform has specific security URL overrides + (#1263) + + -- Brett Holman Wed, 16 Feb 2022 13:39:11 -0700 + +cloud-init (21.4-119-gdeb3ae82-0ubuntu1~22.04.1) jammy; urgency=medium + + * d/cloud-init.templates: Move LXD to back of datasource_list + * New upstream snapshot. + - tests: lsblk --json output changes mountpoint key to mountpoinst [] + (#1261) + - mounts: fix mount opts string for ephemeral disk (#1250) + [Chris Patterson] + - Shell script handlers by freq (#1166) [Chris Lalos] + - minor improvements to documentation (#1259) [Mark Esler] + - cloud-id: publish /run/cloud-init/cloud-id- files (#1244) + - add "eslerm" as contributor (#1258) [Mark Esler] + - sources/azure: refactor ssh key handling (#1248) [Chris Patterson] + - bump pycloudlib (#1256) + - sources/hetzner: Use EphemeralDHCPv4 instead of static configuration + (#1251) [Markus Schade] + - bump pycloudlib version (#1255) [Brett Holman] + - Fix IPv6 netmask format for sysconfig (#1215) [Harald] (LP: #1959148) + - sources/azure: drop debug print (#1249) [Chris Patterson] + - tests: do not check instance.pull_file().ok() (#1246) + - sources/azure: consolidate ephemeral DHCP configuration (#1229) + [Chris Patterson] + - cc_salt_minion freebsd fix for rc.conf (#1236) [Brett Holman] + - sources/azure: fix metadata check in _check_if_nic_is_primary() (#1232) + [Chris Patterson] + - Add _netdev option to mount Azure ephemeral disk (#1213) [Eduardo Otubo] + - testing: stop universally overwriting /etc/cloud/cloud.cfg.d (#1237) + - Integration test changes (#1240) + - Fix Gentoo Locales (#1205) [Brett Holman] + - Add "slingamn" as contributor (#1235) [Shivaram Lingamneni] + - integration: do not LXD bind mount /etc/cloud/cloud.cfg.d (#1234) + - Integration testing docs and refactor (#1231) + - vultr: Return metadata immediately when found (#1233) [eb3095] + - spell check docs with spellintian (#1223) [Brett Holman] + - docs: include upstream python version info (#1230) + - Schema a d (#1211) + - Move LXD to end ds-identify DSLIST (#1228) (LP: #1959118) + - fix parallel tox execution (#1214) [Brett Holman] + - sources/azure: refactor _report_ready_if_needed and _poll_imds (#1222) + [Chris Patterson] + - Do not support setting up archive.canonical.com as a source (#1219) + [Steve Langasek] (LP: #1959343) + - Vultr: Fix lo being used for DHCP, try next on cmd fail (#1208) [eb3095] + - sources/azure: refactor _should_reprovision[_after_nic_attach]() logic + (#1206) [Chris Patterson] + - update ssh logs to show ssh private key gens pub and simplify code + (#1221) [Steve Weber] + - Remove mitechie from stale PR github action (#1217) + - Include POST format in cc_phone_home docs (#1218) (LP: #1959149) + - Add json parsing of ip addr show (SC-723) (#1210) + - cc_rsyslog: fix typo in docstring (#1207) [Louis Sautier] + - Update .github-cla-signers (#1204) [Chris Lalos] + - sources/azure: drop unused case in _report_failure() (#1200) + [Chris Patterson] + - sources/azure: always initialize _ephemeral_dhcp_ctx on unpickle (#1199) + [Chris Patterson] + - Add support for gentoo templates and cloud.cfg (#1179) [vteratipally] + - sources/azure: unpack ret tuple in crawl_metadata() (#1194) + [Chris Patterson] + - tests: focal caplog has whitespace indentation for multi-line logs + (#1201) + - Seek interfaces, skip dummy interface, fix region codes (#1192) [eb3095] + - integration: test against the Ubuntu daily images (#1198) + [Paride Legovini] + - cmd: status and cloud-id avoid change in behavior for 'not run' (#1197) + - tox: pass PYCLOUDLIB_* env vars into integration tests when present + (#1196) + - sources/azure: set ovf_is_accessible when OVF is read successfully + (#1193) [Chris Patterson] + - Enable OVF environment transport via ISO in example (#1195) [Megian] + - sources/azure: consolidate DHCP variants to EphemeralDHCPv4WithReporting + (#1190) [Chris Patterson] + - Single JSON schema validation in early boot (#1175) + - Add DatasourceOVF network-config propery to Ubuntu OVF example (#1184) + [Megian] + - testing: support pycloudlib config file (#1189) + - Ensure system_cfg read before ds net config on Oracle (SC-720) (#1174) + (LP: #1956788) + - Test Optimization Proposal (SC-736) (#1188) [Brett Holman] + - cli: cloud-id report not-run or disabled state as cloud-id (#1162) + - Remove distutils usage (#1177) [Shreenidhi Shedi] + - add .python-version to gitignore (#1186) [Brett Holman] + - print error if datasource import fails (#1170) + [Emanuele Giuseppe Esposito] + - Add new config module to set keyboard layout (#1176) + [maxnet] (LP: #1951593) + - sources/azure: rename metadata_type -> MetadataType (#1181) + [Chris Patterson] + - Remove 3.5 and xenial support (SC-711) (#1167) + - tests: mock LXD datasource detection in ds-identify on LXD containers + (#1178) + - pylint: silence errors on compat code for old jsonschema (#1172) + [Paride Legovini] + - testing: Add 3.10 Test Coverage (#1173) [Brett Holman] + - Remove unittests from integration test job in travis (#1141) + [Brett Holman] + - Don't throw exceptions for empty cloud config (#1130) [Brett Holman] + - bsd/resolv.d/ avoid duplicated entries (#1163) [Gonéri Le Bouder] + - sources/azure: do not persist failed_desired_api_version flag (#1159) + [Chris Patterson] + - Update cc_ubuntu_advantage calls to assume-yes (#1158) + [John Chittum] (LP: #1954842) + - openbsd: properly restart the network on 7.0 (#1150) [Gonéri Le Bouder] + - Add .git-blame-ignore-revs (#1161) + - Adopt Black and isort (SC-700) (#1157) + - Include dpkg frontend lock in APT_LOCK_FILES (#1153) + - tests/cmd/query: fix test run as root and add coverage for defaults + (#1156) [Chris Patterson] (LP: #1825027) + - Schema processing changes (SC-676) (#1144) + - Add dependency workaround for impish in bddeb (#1148) [Brett Holman] + - netbsd: install new dep packages (#1151) [Gonéri Le Bouder] + - find_devs_with_openbsd: ensure we return the last entry (#1149) + [Gonéri Le Bouder] + - sources/azure: remove unnecessary hostname bounce (#1143) + [Chris Patterson] + - find_devs/openbsd: accept ISO on disk (#1132) + [Gonéri Le Bouder] (GH: + https://github.com/ContainerCraft/kmi/issues/12) + - Improve error log message when mount failed (#1140) [Ksenija Stanojevic] + - add KsenijaS as a contributor (#1145) [Ksenija Stanojevic] + - travis - don't run integration tests if no deb (#1139) [Brett Holman] + - factor out function for getting top level directory of cloudinit (#1136) + [Brett Holman] + - testing: Add deterministic test id (#1138) [Brett Holman] + - mock sleep() in azure test (#1137) [Brett Holman] + - Add miraclelinux support (#1128) [Haruki TSURUMOTO] + - docs: Make MACs lowercase in network config (#1135) (GH: #1876941) + - Add Strict Metaschema Validation (#1101) [Brett Holman] + - update dead link (#1133) [Brett Holman] + - cloudinit/net: handle two different routes for the same ip (#1124) + [Emanuele Giuseppe Esposito] + - docs: pin mistune dependency (#1134) + + -- Chad Smith Thu, 10 Feb 2022 16:02:56 -0700 + +cloud-init (21.4-25-g039c40f9-0ubuntu1~22.04.1) jammy; urgency=medium + + * New upstream snapshot. + - Reorganize unit test locations under tests/unittests (#1126) + [Brett Holman] + - Fix exception when no activator found (#1129) (LP: #1948681) + - jinja: provide and document jinja-safe key aliases in instance-data + (SC-622) (#1123) + - testing: Remove date from final_message test (SC-638) (#1127) + - Move GCE metadata fetch to init-local (SC-502) (#1122) + - Fix missing metadata routes for vultr (#1125) [eb3095] + - cc_ssh_authkey_fingerprints.py: prevent duplicate messages on console + (#1081) [dermotbradley] + - sources/azure: remove unused remnants related to agent command (#1119) + [Chris Patterson] + - github: update PR template's contributing URL (#1120) [Chris Patterson] + - docs: Rename HACKING.rst to CONTRIBUTING.rst (#1118) + - testing: monkeypatch system_info call in unit tests (SC-533) (#1117) + - Fix Vultr timeout and wait values (#1113) [eb3095] + - lxd: add preference for LXD cloud-init.* config keys over user keys + (#1108) + - VMware: source /etc/network/interfaces.d/* on Debian + [chengcheng-chcheng] (LP: #1950136) + - Add cjp256 as contributor (#1109) [Chris Patterson] + - integration_tests: Ensure log directory exists before symlinking to it + (#1110) + - testing: add growpart integration test (#1104) [Brett Holman] + - integration_test: Speed up CI run time (#1111) + - Some miscellaneous integration test fixes (SC-606) (#1103) + - tests: specialize lxd_discovery test for lxd_vm vendordata (#1106) + - Add convenience symlink to integration test output (#1105) [Brett Holman] + - Fix for set-name bug in networkd renderer (#1100) + [Andrew Kutz] (LP: #1949407) + - Wait for apt lock (#1034) (LP: #1944611) + - testing: stop chef test from running on openstack (#1102) + - alpine.py: add options to the apk upgrade command (#1089) [dermotbradley] + + -- Chad Smith Fri, 03 Dec 2021 15:44:06 -0700 + +cloud-init (21.4-0ubuntu1~22.04.1) jammy; urgency=medium + + * d/upstream/metadata: Change contact to James Falcon + * d/cloud-init.templates: Add LXD to default datasource_list with + translations + * drop the following cherry-picks now included: + + cpick-28e56d99-Azure-Retry-dhcp-on-timeouts-when-polling + + cpick-e69a8874-Set-Azure-to-only-update-metadata-on-BOOT_NEW_INSTANCE + + cpick-612e3908-Add-connectivity_url-to-Oracle-s-EphemeralDHCPv4-988 + + cpick-dc227869-Set-Azure-to-apply-networking-config-every-BOOT-1023 + + cpick-9c147e83-Allow-disabling-of-network-activation-SC-307-1048 + * New upstream release. + - Release 21.4 (#1091) (LP: #1949405) + - Azure: fallback nic needs to be reevaluated during reprovisioning + (#1094) [Anh Vo] + - azure: pps imds (#1093) [Anh Vo] + - testing: Remove calls to 'install_new_cloud_init' (#1092) + - Add LXD datasource (#1040) + - Fix unhandled apt_configure case. (#1065) [Brett Holman] + - Allow libexec for hotplug (#1088) + - Add necessary mocks to test_ovf unit tests (#1087) + - Remove (deprecated) apt-key (#1068) [Brett Holman] (LP: #1836336) + - distros: Remove a completed "TODO" comment (#1086) + - cc_ssh.py: Add configuration for controlling ssh-keygen output (#1083) + [dermotbradley] + - Add "install hotplug" module (SC-476) (#1069) (LP: #1946003) + - hosts.alpine.tmpl: rearrange the order of short and long hostnames + (#1084) [dermotbradley] + - Add max version to docutils + - cloudinit/dmi.py: Change warning to debug to prevent console display + (#1082) [dermotbradley] + - remove unnecessary EOF string in + disable-sshd-keygen-if-cloud-init-active.conf (#1075) [Emanuele + Giuseppe Esposito] + - Add module 'write-files-deferred' executed in stage 'final' (#916) + [Lucendio] + - Bump pycloudlib to fix CI (#1080) + - Remove pin in dependencies for jsonschema (#1078) + - Add "Google" as possible system-product-name (#1077) [vteratipally] + - Update Debian security suite for bullseye (#1076) [Johann Queuniet] + - Leave the details of service management to the distro (#1074) + [Andy Fiddaman] + - Fix typos in setup.py (#1059) [Christian Clauss] + - Update Azure _unpickle (SC-500) (#1067) (LP: #1946644) + - cc_ssh.py: fix private key group owner and permissions (#1070) + [Emanuele Giuseppe Esposito] + - VMware: read network-config from ISO (#1066) [Thomas Weißschuh] + - testing: mock sleep in gce unit tests (#1072) + - CloudStack: fix data-server DNS resolution (#1004) + [Olivier Lemasle] (LP: #1942232) + - Fix unit test broken by pyyaml upgrade (#1071) + - testing: add get_cloud function (SC-461) (#1038) + - Inhibit sshd-keygen@.service if cloud-init is active (#1028) + [Ryan Harper] + - VMWARE: search the deployPkg plugin in multiarch dir (#1061) + [xiaofengw-vmware] (LP: #1944946) + - Fix set-name/interface DNS bug (#1058) [Andrew Kutz] (LP: #1946493) + - Use specified tmp location for growpart (#1046) [jshen28] + - .gitignore: ignore tags file for ctags users (#1057) [Brett Holman] + - Allow comments in runcmd and report failed commands correctly (#1049) + [Brett Holman] (LP: #1853146) + - tox integration: pass the *_proxy, GOOGLE_*, GCP_* env vars (#1050) + [Paride Legovini] + - Allow disabling of network activation (SC-307) (#1048) (LP: #1938299) + - renderer: convert relative imports to absolute (#1052) [Paride Legovini] + - Support ETHx_IP6_GATEWAY, SET_HOSTNAME on OpenNebula (#1045) + [Vlastimil Holer] + - integration-requirements: bump the pycloudlib commit (#1047) + [Paride Legovini] + - Allow Vultr to set MTU and use as-is configs (#1037) [eb3095] + - pin jsonschema in requirements.txt (#1043) + - testing: remove cloud_tests (#1020) + - Add andgein as contributor (#1042) [Andrew Gein] + - Make wording for module frequency consistent (#1039) [Nicolas Bock] + - Use ascii code for growpart (#1036) [jshen28] + - Add jshen28 as contributor (#1035) [jshen28] + - Skip test_cache_purged_on_version_change on Azure (#1033) + - Remove invalid ssh_import_id from examples (#1031) + - Cleanup Vultr support (#987) [eb3095] + - docs: update cc_disk_setup for fs to raw disk (#1017) + - HACKING.rst: change contact info to James Falcon (#1030) + - tox: bump the pinned flake8 and pylint version (#1029) + [Paride Legovini] (LP: #1944414) + - Add retries to DataSourceGCE.py when connecting to GCE (#1005) + [vteratipally] + - Set Azure to apply networking config every BOOT (#1023) + - Add connectivity_url to Oracle's EphemeralDHCPv4 (#988) (LP: #1939603) + - docs: fix typo and include sudo for report bugs commands (#1022) + [Renan Rodrigo] (LP: #1940236) + - VMware: Fix typo introduced in #947 and add test (#1019) [PengpengSun] + - Update IPv6 entries in /etc/hosts (#1021) [Richard Hansen] (LP: #1943798) + - Integration test upgrades for the 21.3-1 SRU (#1001) + - Add Jille to tools/.github-cla-signers (#1016) [Jille Timmermans] + - Improve ug_util.py (#1013) [Shreenidhi Shedi] + - Support openEuler OS (#1012) [zhuzaifangxuele] + - ssh_utils.py: ignore when sshd_config options are not key/value pairs + (#1007) [Emanuele Giuseppe Esposito] + - Set Azure to only update metadata on BOOT_NEW_INSTANCE (#1006) + - cc_update_etc_hosts: Use the distribution-defined path for the hosts + file (#983) [Andy Fiddaman] + - Add CloudLinux OS support (#1003) [Alexandr Kravchenko] + - puppet config: add the start_agent option (#1002) [Andrew Bogott] + - Fix `make style-check` errors (#1000) [Shreenidhi Shedi] + - Make cloud-id copyright year (#991) [Andrii Podanenko] + - Add support to accept-ra in networkd renderer (#999) [Shreenidhi Shedi] + - Update ds-identify to pass shellcheck (#979) [Andrew Kutz] + - Azure: Retry dhcp on timeouts when polling reprovisiondata (#998) + [aswinrajamannar] + + -- James Falcon Tue, 02 Nov 2021 18:07:49 -0500 + +cloud-init (21.3-1-g6803368d-0ubuntu3) impish; urgency=medium + + * cherry-pick 9c147e83: Allow disabling of network activation (SC-307) + (#1048) (LP: #1938299) + * cherry-pick 612e3908: Add connectivity_url to Oracle's + EphemeralDHCPv4 (#988) (LP: #1939603) + * cherry-pick dc227869: Set Azure to apply networking config every BOOT + (#1023) + + -- James Falcon Thu, 07 Oct 2021 11:43:55 -0500 + +cloud-init (21.3-1-g6803368d-0ubuntu2) impish; urgency=medium + + * cherry-pick 28e56d99: Azure: Retry dhcp on timeouts when polling + reprovisiondata + * cherry-pick e69a8874: Set Azure to only update metadata on + BOOT_NEW_INSTANCE + + -- James Falcon Fri, 03 Sep 2021 14:36:41 -0500 + +cloud-init (21.3-1-g6803368d-0ubuntu1) impish; urgency=medium + + * New upstream snapshot. + - testing: Fix ssh keys integration test (#992) + - Release 21.3 (#993) (LP: #1940839) + - Azure: During primary nic detection, check interface status continuously + before rebinding again (#990) [aswinrajamannar] + - Fix home permissions modified by ssh module (SC-338) (#984) + (LP: #1940233) + - Add integration test for sensitive jinja substitution (#986) + - Ignore hotplug socket when collecting logs (#985) (LP: #1940235) + - testing: Add missing mocks to test_vmware.py (#982) + - add Zadara Edge Cloud Platform to the supported clouds list (#963) + [sarahwzadara] + - testing: skip upgrade tests on LXD VMs (#980) + + -- James Falcon Mon, 23 Aug 2021 16:53:23 -0500 + +cloud-init (21.2-69-g65607405-0ubuntu1) impish; urgency=medium + + * d/cloud-init.templates: Add VMware to default datasource_list + * d/control: Add dependencies on python3-netifaces for vmware ds + * New upstream snapshot. + - Only invoke hotplug socket when functionality is enabled (#952) + - Revert unnecesary lcase in ds-identify (#978) [Andrew Kutz] + - cc_resolv_conf: fix typos (#969) [Shreenidhi Shedi] + - Replace broken httpretty tests with mock (SC-324) (#973) + - Azure: Check if interface is up after sleep when trying to bring it up + (#972) [aswinrajamannar] + - Update dscheck_VMware's rpctool check (#970) [Shreenidhi Shedi] + - Azure: Logging the detected interfaces (#968) [Moustafa Moustafa] + - Change netifaces dependency to 0.10.4 (#965) [Andrew Kutz] + - Azure: Limit polling network metadata on connection errors (#961) + [aswinrajamannar] + - Update inconsistent indentation (#962) [Andrew Kutz] + - cc_puppet: support AIO installations and more (#960) [Gabriel Nagy] + - Add Puppet contributors to CLA signers (#964) [Noah Fontes] + - Datasource for VMware (#953) [Andrew Kutz] + - photon: refactor hostname handling and add networkd activator (#958) + [sshedi] + - Stop copying ssh system keys and check folder permissions (#956) + [Emanuele Giuseppe Esposito] + - testing: port remaining cloud tests to integration testing framework + (SC-191) (#955) + - generate contents for ovf-env.xml when provisioning via IMDS (#959) + [Anh Vo] + - Add support for EuroLinux 7 && EuroLinux 8 (#957) [Aleksander Baranowski] + - Implementing device_aliases as described in docs (#945) + [Mal Graty] (LP: #1867532) + - testing: fix test_ssh_import_id.py (#954) + - Add ability to manage fallback network config on PhotonOS (#941) [sshedi] + - Add VZLinux support (#951) [eb3095] + - VMware: add network-config support in ovf-env.xml (#947) [PengpengSun] + - Update pylint to v2.9.3 and fix the new issues it spots (#946) + [Paride Legovini] + - Azure: mount default provisioning iso before try device listing (#870) + [Anh Vo] + - Document known hotplug limitations (#950) + + -- James Falcon Fri, 13 Aug 2021 15:37:31 -0500 + +cloud-init (21.2-43-g184c836a-0ubuntu1) impish; urgency=medium + + * New upstream snapshot. + - Initial hotplug support (#936) + - Fix MIME policy failure on python version upgrade (#934) + - run-container: fixup the centos repos baseurls when using http_proxy + (#944) [Paride Legovini] + - tools: add support for building rpms on rocky linux (#940) + - ssh-util: allow cloudinit to merge all ssh keys into a custom user + file, defined in AuthorizedKeysFile (#937) [Emanuele Giuseppe Esposito] + (LP: #1911680) + - VMware: new "allow_raw_data" switch (#939) [xiaofengw-vmware] + - bump pycloudlib version (#935) + - add renanrodrigo as a contributor (#938) [Renan Rodrigo] + - testing: simplify test_upgrade.py (#932) + - freebsd/net_v1 format: read MTU from root (#930) [Gonéri Le Bouder] + - Add new network activators to bring up interfaces (#919) + - Detect a Python version change and clear the cache (#857) + [Robert Schweikert] + - cloud_tests: fix the Impish release name (#931) [Paride Legovini] + - Removed distro specific network code from Photon (#929) [sshedi] + - Add support for VMware PhotonOS (#909) [sshedi] + - cloud_tests: add impish release definition (#927) [Paride Legovini] + - docs: fix stale links rename master branch to main (#926) + - Fix DNS in NetworkState (SC-133) (#923) + - tests: Add 'adhoc' mark for integration tests (#925) + - Fix the spelling of "DigitalOcean" (#924) [Mark Mercado] + - Small Doc Update for ReportEventStack and Test (#920) [Mike Russell] + - Replace deprecated collections.Iterable with abc replacement (#922) + (LP: #1932048) + - testing: OCI availability domain is now required (SC-59) (#910) + - add DragonFlyBSD support (#904) [Gonéri Le Bouder] + - Use instance-data-sensitive.json in jinja templates (SC-117) (#917) + (LP: #1931392) + - doc: Update NoCloud docs stating required files (#918) (LP: #1931577) + - build-on-netbsd: don't pin a specific py3 version (#913) + [Gonéri Le Bouder] + - - Create the log file with 640 permissions (#858) [Robert Schweikert] + - Allow braces to appear in dhclient output (#911) [eb3095] + - Docs: Replace all freenode references with libera (#912) + - openbsd/net: flush the route table on net restart (#908) + [Gonéri Le Bouder] + - Add Rocky Linux support to cloud-init (#906) [Louis Abel] + - Add "esposem" as contributor (#907) [Emanuele Giuseppe Esposito] + - Add integration test for #868 (#901) + - Added support for importing keys via primary/security mirror clauses + (#882) [Paul Goins] (LP: #1925395) + - [examples] config-user-groups expire in the future (#902) + [Geert Stappers] + - BSD: static network, set the mtu (#894) [Gonéri Le Bouder] + - Add integration test for lp-1920939 (#891) + - Fix unit tests breaking from new httpretty version (#903) + - Allow user control over update events (#834) + + -- James Falcon Mon, 19 Jul 2021 14:23:12 -0500 + +cloud-init (21.2-3-g899bfaa9-0ubuntu2) impish; urgency=medium + + * d/po/templates.pot: refresh with debconf-updatepo + + -- Daniel Watkins Wed, 12 May 2021 12:23:42 -0400 + +cloud-init (21.2-3-g899bfaa9-0ubuntu1) impish; urgency=medium + + * drop the following cherry-picks now included: + + cpick-83f6bbfb-Fix-unpickle-for-source-paths-missing-run_dir-863 + + cpick-d132356c-fix-error-on-upgrade-caused-by-new-vendordata2 + * New upstream snapshot. + - Update test characters in substitution unit test (#893) + - cc_disk_setup.py: remove UDEVADM_CMD definition as not used (#886) + [dermotbradley] + - Add AlmaLinux OS support (#872) [Andrew Lukoshko] + - Release 21.2 (#890) (LP: #1927254) + - Add \r\n check for SSH keys in Azure (#889) + - Revert "Add support to resize rootfs if using LVM (#721)" (#887) + (LP: #1922742) + - Add Vultaire as contributor (#881) [Paul Goins] + - Azure: adding support for consuming userdata from IMDS (#884) [Anh Vo] + - test_upgrade: modify test_upgrade_package to run for more sources (#883) + - Fix chef module run failure when chef_license is set (#868) [Ben Hughes] + - Azure: Retry net metadata during nic attach for non-timeout errs (#878) + [aswinrajamannar] + - Azure: Retrieve username and hostname from IMDS (#865) [Thomas Stringer] + - Azure: eject the provisioning iso before reporting ready (#861) [Anh Vo] + - Use `partprobe` to re-read partition table if available (#856) + [Nicolas Bock] (LP: #1920939) + - fix error on upgrade caused by new vendordata2 attributes (#869) + (LP: #1922739) + - add prefer_fqdn_over_hostname config option (#859) + [hamalq] (LP: #1921004) + - Emit dots on travis to avoid timeout (#867) + - doc: Replace remaining references to user-scripts as a config module + (#866) [Ryan Harper] + - azure: Removing ability to invoke walinuxagent (#799) [Anh Vo] + - Add Vultr support (#827) [David Dymko] + - Fix unpickle for source paths missing run_dir (#863) + [lucasmoura] (LP: #1899299) + - sysconfig: use BONDING_MODULE_OPTS on SUSE (#831) [Jens Sandmann] + - bringup_static_routes: fix gateway check (#850) [Petr Fedchenkov] + - add hamalq user (#860) [hamalq] + - Add support to resize rootfs if using LVM (#721) + [Eduardo Otubo] (LP: #1799953) + - Fix mis-detecting network configuration in initramfs cmdline (#844) + (LP: #1919188) + - tools/write-ssh-key-fingerprints: do not display empty header/footer + (#817) [dermotbradley] + - Azure helper: Ensure Azure http handler sleeps between retries (#842) + [Johnson Shi] + - Fix chef apt source example (#826) [timothegenzmer] + + -- James Falcon Tue, 11 May 2021 11:52:54 -0500 + +cloud-init (21.1-19-gbad84ad4-0ubuntu3) hirsute; urgency=medium + + * cherry-pick 83f6bbfb: Fix unpickle for source paths missing run_dir + (#863) (LP: #1899299) + * cherry-pick d132356c: fix error on upgrade caused by new vendordata2 + attributes (LP: #1922739) + + -- James Falcon Mon, 19 Apr 2021 13:41:21 -0500 + +cloud-init (21.1-19-gbad84ad4-0ubuntu2) hirsute; urgency=medium + + * debian/cloud-init.templates: enable UpCloud by default + + -- James Falcon Mon, 22 Mar 2021 14:08:51 -0500 + +cloud-init (21.1-19-gbad84ad4-0ubuntu1) hirsute; urgency=medium + * d/cloud-init.postinst: Change output log permissions on upgrade + (LP: #1918303) + * New upstream snapshot. + - .travis.yml: generate an SSH key before running tests (#848) + - write passwords only to serial console, lock down cloud-init-output.log + (#847) (LP: #1918303) + - Fix apt default integration test (#845) + - integration_tests: bump pycloudlib dependency (#846) + - commit f35181fa970453ba6c7c14575b12185533391b97 [eb3095] + - archlinux: Fix broken locale logic (#841) + [Kristian Klausen] (LP: #1402406) + - Integration test for #783 (#832) + - integration_tests: mount more paths IN_PLACE (#838) + - Fix requiring device-number on EC2 derivatives (#836) (LP: #1917875) + - Remove the vi comment from the part-handler example (#835) + - net: exclude OVS internal interfaces in get_interfaces (#829) + (LP: #1912844) + - tox.ini: pass OS_* environment variables to integration tests (#830) + - integration_tests: add OpenStack as a platform (#804) + - Add flexibility to IMDS api-version (#793) [Thomas Stringer] + - Fix the TestApt tests using apt-key on Xenial and Hirsute (#823) + [Paride Legovini] (LP: #1916629) + - doc: remove duplicate "it" from nocloud.rst (#825) [V.I. Wood] + - archlinux: Use hostnamectl to set the transient hostname (#797) + [Kristian Klausen] + - cc_keys_to_console.py: Add documentation for recently added config key + (#824) [dermotbradley] + - Update cc_set_hostname documentation (#818) [Toshi Aoyama] + + -- James Falcon Fri, 19 Mar 2021 14:32:13 -0500 + +cloud-init (21.1-0ubuntu1) hirsute; urgency=medium + + * New upstream release. + - Release 21.1 (#820) (LP: #1916540) + - Azure: Support for VMs without ephemeral resource disks. (#800) + [Johnson Shi] (LP: #1901011) + - cc_keys_to_console: add option to disable key emission (#811) + [Michael Hudson-Doyle] (LP: #1915460) + - integration_tests: introduce lxd_use_exec mark (#802) + - azure: case-insensitive UUID to avoid new IID during kernel upgrade + (#798) (LP: #1835584) + - stale.yml: don't ask submitters to reopen PRs (#816) + - integration_tests: fix use of SSH agent within tox (#815) + - integration_tests: add UPGRADE CloudInitSource (#812) + - integration_tests: use unique MAC addresses for tests (#813) + - Update .gitignore (#814) + - Port apt cloud_tests to integration tests (#808) + - integration_tests: fix test_gh626 on LXD VMs (#809) + - Fix attempting to decode binary data in test_seed_random_data test (#806) + - Remove wait argument from tests with session_cloud calls (#805) + - Datasource for UpCloud (#743) [Antti Myyrä] + - test_gh668: fix failure on LXD VMs (#801) + - openstack: read the dynamic metadata group vendor_data2.json (#777) + [Andrew Bogott] (LP: #1841104) + - includedir in suoders can be prefixed by "arroba" (#783) + [Jordi Massaguer Pla] + + -- James Falcon Tue, 23 Feb 2021 10:06:10 -0500 + +cloud-init (20.4.1-79-g71564dce-0ubuntu1) hirsute; urgency=medium + + * New upstream snapshot. + - Release 20.4.1 (LP: #1911680) + - [VMware] change default max wait time to 15s (#774) [xiaofengw-vmware] + - Revert integration test associated with reverted #586 (#784) + - Add jordimassaguerpla as contributor (#787) [Jordi Massaguer Pla] + - Add Rick Harding to CLA signers (#792) [Rick Harding] + - HACKING.rst: add clarifying note to LP CLA process section (#789) + - Stop linting cloud_tests (#791) + - cloud-tests: update cryptography requirement (#790) [Joshua Powers] + - Remove 'remove-raise-on-failure' calls from integration_tests (#788) + - Use more cloud defaults in integration tests (#757) + - Adding self to cla signers (#776) [Andrew Bogott] + - doc: avoid two warnings (#781) [Dan Kenigsberg] + - Use proper spelling for Red Hat (#778) [Dan Kenigsberg] + + -- Daniel Watkins Thu, 28 Jan 2021 10:50:23 -0500 + +cloud-init (20.4-66-gad23441d-0ubuntu1) hirsute; urgency=medium + + * New upstream snapshot. + - Add antonyc to .github-cla-signers (#747) [Anton Chaporgin] + - integration_tests: log image serial if available (#772) + - Revert "ssh_util: handle non-default AuthorizedKeysFile config (#586)" + (#775) (LP: #1911680) + - [VMware] Support cloudinit raw data feature (#691) [xiaofengw-vmware] + - net: Fix static routes to host in eni renderer (#668) [Pavel Abalikhin] + - .travis.yml: don't run cloud_tests in CI (#756) + - test_upgrade: add some missing commas (#769) + - cc_seed_random: update documentation and fix integration test (#771) + (LP: #1911227) + - Fix test gh-632 test to only run on NoCloud (#770) (LP: #1911230) + - archlinux: fix package upgrade command handling (#768) [Bao Trinh] + - integration_tests: add integration test for LP: #1910835 (#761) + + -- Daniel Watkins Wed, 20 Jan 2021 13:15:55 -0500 + +cloud-init (20.4-55-g4f62ae8d-0ubuntu1) hirsute; urgency=medium + + * New upstream snapshot. + - Fix regression with handling of IMDS ssh keys (#760) [Thomas Stringer] + - integration_tests: log cloud-init version in SUT (#758) + - Add ajmyyra as contributor (#742) [Antti Myyrä] + - net_convert: add some missing help text (#755) + - Missing IPV6_AUTOCONF=no to render sysconfig dhcp6 stateful on RHEL + (#753) [Eduardo Otubo] + - doc: document missing IPv6 subnet types (#744) [Antti Myyrä] + - Add example configuration for datasource `AliYun` (#751) [Xiaoyu Zhong] + - integration_tests: add SSH key selection settings (#754) + - fix a typo in man page cloud-init.1 (#752) [Amy Chen] + - network-config-format-v2.rst: add Netplan Passthrough section (#750) + - stale: re-enable post holidays (#749) + - integration_tests: port ca_certs tests from cloud_tests (#732) + - Azure: Add telemetry for poll IMDS (#741) [Johnson Shi] + - doc: move testing section from HACKING to its own doc (#739) + - No longer allow integration test failures on travis (#738) + - stale: fix error in definition (#740) + - integration_tests: set log-cli-level to INFO by default (#737) + - PULL_REQUEST_TEMPLATE.md: use backticks around commit message (#736) + - stale: disable check for holiday break (#735) + - integration_tests: log the path we collect logs into (#733) + - .travis.yml: add (most) supported Python versions to CI (#734) + - integration_tests: fix IN_PLACE CLOUD_INIT_SOURCE (#731) + - cc_ca_certs: add RHEL support (#633) [cawamata] + - Azure: only generate config for NICs with addresses (#709) + [Thomas Stringer] + - doc: fix CloudStack configuration example (#707) [Olivier Lemasle] + - integration_tests: restrict test_lxd_bridge appropriately (#730) + - Add integration tests for CLI functionality (#729) + - Integration test for gh-626 (#728) + - Some test_upgrade fixes (#726) + - Ensure overriding test vars with env vars works for booleans (#727) + - integration_tests: port lxd_bridge test from cloud_tests (#718) + - Integration test for gh-632. (#725) + - Integration test for gh-671 (#724) + - integration-requirements.txt: bump pycloudlib commit (#723) + - Drop unnecessary shebang from cmd/main.py (#722) [Eduardo Otubo] + - Integration test for LP: #1813396 and #669 (#719) + - integration_tests: include timestamp in log output (#720) + - integration_tests: add test for LP: #1898997 (#713) + - Add integration test for power_state_change module (#717) + - Update documentation for network-config-format-v2 (#701) [ggiesen] + - sandbox CA Cert tests to not require ca-certificates (#715) + [Eduardo Otubo] + - Add upgrade integration test (#693) + - Integration test for 570 (#712) + - Add ability to keep snapshotted images in integration tests (#711) + - Integration test for pull #586 (#706) + - integration_tests: introduce skipping of tests by OS (#702) + - integration_tests: introduce IntegrationInstance.restart (#708) + - Add lxd-vm to list of valid integration test platforms (#705) + - Adding BOOTPROTO = dhcp to render sysconfig dhcp6 stateful on RHEL + (#685) [Eduardo Otubo] + - Delete image snapshots created for integration tests (#682) + - Parametrize ssh_keys_provided integration test (#700) [lucasmoura] + - Drop use_sudo attribute on IntegrationInstance (#694) [lucasmoura] + - cc_apt_configure: add riscv64 as a ports arch (#687) + [Dimitri John Ledkov] + - cla: add xnox (#692) [Dimitri John Ledkov] + - Collect logs from integration test runs (#675) + + -- Daniel Watkins Mon, 11 Jan 2021 16:57:30 -0500 + +cloud-init (20.4-0ubuntu2) hirsute; urgency=medium + + * d/cloud-init.manpages: include upstream manpages in package (LP: #1908548) + + -- Daniel Watkins Mon, 04 Jan 2021 10:44:13 -0500 + +cloud-init (20.4-0ubuntu1) hirsute; urgency=medium + + * d/control: add gnupg to Recommends as cc_apt_configure requires it to be + installed for some operations. + * New upstream release. + - Release 20.4 (#686) [James Falcon] (LP: #1905440) + - tox: avoid tox testenv subsvars for xenial support (#684) + - Ensure proper root permissions in integration tests (#664) [James Falcon] + - LXD VM support in integration tests (#678) [James Falcon] + - Integration test for fallocate falling back to dd (#681) [James Falcon] + - .travis.yml: correctly integration test the built .deb (#683) + - Ability to hot-attach NICs to preprovisioned VMs before reprovisioning + (#613) [aswinrajamannar] + - Support configuring SSH host certificates. (#660) [Jonathan Lung] + - add integration test for LP: #1900837 (#679) + - cc_resizefs on FreeBSD: Fix _can_skip_ufs_resize (#655) + [Mina Galić] (LP: #1901958, #1901958) + - DataSourceAzure: push dmesg log to KVP (#670) [Anh Vo] + - Make mount in place for tests work (#667) [James Falcon] + - integration_tests: restore emission of settings to log (#657) + - DataSourceAzure: update password for defuser if exists (#671) [Anh Vo] + - tox.ini: only select "ci" marked tests for CI runs (#677) + - Azure helper: Increase Azure Endpoint HTTP retries (#619) [Johnson Shi] + - DataSourceAzure: send failure signal on Azure datasource failure (#594) + [Johnson Shi] + - test_persistence: simplify VersionIsPoppedFromState (#674) + - only run a subset of integration tests in CI (#672) + - cli: add --system param to allow validating system user-data on a + machine (#575) + - test_persistence: add VersionIsPoppedFromState test (#673) + - introduce an upgrade framework and related testing (#659) + - add --no-tty option to gpg (#669) [Till Riedel] (LP: #1813396) + - Pin pycloudlib to a working commit (#666) [James Falcon] + - DataSourceOpenNebula: exclude SRANDOM from context output (#665) + - cloud_tests: add hirsute release definition (#662) + - split integration and cloud_tests requirements (#652) + - faq.rst: add warning to answer that suggests running `clean` (#661) + - Fix stacktrace in DataSourceRbxCloud if no metadata disk is found (#632) + [Scott Moser] + - Make wakeonlan Network Config v2 setting actually work (#626) + [dermotbradley] + - HACKING.md: unify network-refactoring namespace (#658) [Mina Galić] + - replace usage of dmidecode with kenv on FreeBSD (#621) [Mina Galić] + - Prevent timeout on travis integration tests. (#651) [James Falcon] + - azure: enable pushing the log to KVP from the last pushed byte (#614) + [Moustafa Moustafa] + - Fix launch_kwargs bug in integration tests (#654) [James Falcon] + - split read_fs_info into linux & freebsd parts (#625) [Mina Galić] + - PULL_REQUEST_TEMPLATE.md: expand commit message section (#642) + - Make some language improvements in growpart documentation (#649) + [Shane Frasier] + - Revert ".travis.yml: use a known-working version of lxd (#643)" (#650) + - Fix not sourcing default 50-cloud-init ENI file on Debian (#598) + [WebSpider] + - remove unnecessary reboot from gpart resize (#646) [Mina Galić] + - cloudinit: move dmi functions out of util (#622) [Scott Moser] + - integration_tests: various launch improvements (#638) + - test_lp1886531: don't assume /etc/fstab exists (#639) + - Remove Ubuntu restriction from PR template (#648) [James Falcon] + - util: fix mounting of vfat on *BSD (#637) [Mina Galić] + - conftest: improve docstring for disable_subp_usage (#644) + - doc: add example query commands to debug Jinja templates (#645) + - Correct documentation and testcase data for some user-data YAML (#618) + [dermotbradley] + - Hetzner: Fix instance_id / SMBIOS serial comparison (#640) + [Markus Schade] + - .travis.yml: use a known-working version of lxd (#643) + - tools/build-on-freebsd: fix comment explaining purpose of the script + (#635) [Mina Galić] + - Hetzner: initialize instance_id from system-serial-number (#630) + [Markus Schade] (LP: #1885527) + - Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on static6 (#634) + [Eduardo Otubo] + - get_interfaces: don't exclude Open vSwitch bridge/bond members (#608) + [Lukas Märdian] (LP: #1898997) + - Add config modules for controlling IBM PowerVM RMC. (#584) + [Aman306] (LP: #1895979) + - Update network config docs to clarify MAC address quoting (#623) + [dermotbradley] + - gentoo: fix hostname rendering when value has a comment (#611) + [Manuel Aguilera] + - refactor integration testing infrastructure (#610) [James Falcon] + - stages: don't reset permissions of cloud-init.log every boot (#624) + (LP: #1900837) + - docs: Add how to use cloud-localds to boot qemu (#617) [Joshua Powers] + - Drop vestigial update_resolve_conf_file function (#620) [Scott Moser] + - cc_mounts: correctly fallback to dd if fallocate fails (#585) + (LP: #1897099) + - .travis.yml: add integration-tests to Travis matrix (#600) + - ssh_util: handle non-default AuthorizedKeysFile config (#586) + [Eduardo Otubo] + - Multiple file fix for AuthorizedKeysFile config (#60) [Eduardo Otubo] + - bddeb: new --packaging-branch argument to pull packaging from branch + (#576) [Paride Legovini] + - Add more integration tests (#615) [lucasmoura] + - DataSourceAzure: write marker file after report ready in preprovisioning + (#590) [Johnson Shi] + - integration_tests: emit settings to log during setup (#601) + - integration_tests: implement citest tests run in Travis (#605) + - Add Azure support to integration test framework (#604) [James Falcon] + - openstack: consider product_name as valid chassis tag (#580) + [Adrian Vladu] (LP: #1895976) + - azure: clean up and refactor report_diagnostic_event (#563) [Johnson Shi] + - net: add the ability to blacklist network interfaces based on driver + during enumeration of physical network devices (#591) [Anh Vo] + - integration_tests: don't error on cloud-init failure (#596) + - integration_tests: improve cloud-init.log assertions (#593) + - conftest.py: remove top-level import of httpretty (#599) + - tox.ini: add integration-tests testenv definition (#595) + - PULL_REQUEST_TEMPLATE.md: empty checkboxes need a space (#597) + - add integration test for LP: #1886531 (#592) + - Initial implementation of integration testing infrastructure (#581) + [James Falcon] + - Fix name of ntp and chrony service on CentOS and RHEL. (#589) + [Scott Moser] (LP: #1897915) + - Adding a PR template (#587) [James Falcon] + - Azure parse_network_config uses fallback cfg when generate IMDS network + cfg fails (#549) [Johnson Shi] + - features: refresh docs for easier out-of-context reading (#582) + - Fix typo in resolv_conf module's description (#578) [Wacław Schiller] + - cc_users_groups: minor doc formatting fix (#577) + - Fix typo in disk_setup module's description (#579) [Wacław Schiller] + - Add vendor-data support to seedfrom parameter for NoCloud and OVF (#570) + [Johann Queuniet] + - boot.rst: add First Boot Determination section (#568) (LP: #1888858) + - opennebula.rst: minor readability improvements (#573) [Mina Galić] + - cloudinit: remove unused LOG variables (#574) + + -- James Falcon Tue, 24 Nov 2020 12:32:00 -0600 + +cloud-init (20.3-15-g6d332e5c-0ubuntu1) groovy; urgency=medium + + * d/cloud-init.postinst: fix the grub install device for NVMe-rooted + instances on upgrade. (LP: #1889555) + * d/cloud-init.templates: add RbxCloud to Choices-C. + * Add d/clean to fully clean the build artifacts. + * d/control: + - Bump Standards-Version to 4.5.0, no changes needed. + - B-D on debhelper-compat; drop d/compat. + * Bump the debhelper compat level to 13. Required changes: + - Stop including the dh systemd plugin. + - Switch from dh_systemd_start to dh_installsystemd + * New upstream snapshot. + - create a shutdown_command method in distro classes (#567) + [Emmanuel Thomé] + - user_data: remove unused constant (#566) + - network: Fix type and respect name when rendering vlan in + sysconfig. (#541) [Eduardo Otubo] (LP: #1788915, #1826608) + - Retrieve SSH keys from IMDS first with OVF as a fallback (#509) + [Thomas Stringer] + - Add jqueuniet as contributor (#569) [Johann Queuniet] + - distros: minor typo fix (#562) + - Bump the integration-requirements versioned dependencies (#565) + [Paride Legovini] + - network-config-format-v1: fix typo in nameserver example (#564) + [Stanislas] + - Run cloud-init-local.service after the hv_kvp_daemon (#505) + [Robert Schweikert] + - Add method type hints for Azure helper (#540) [Johnson Shi] + - systemd: add Before=shutdown.target when Conflicts=shutdown.target is + used (#546) [Paride Legovini] + - LXD: detach network from profile before deleting it (#542) + [Paride Legovini] (LP: #1776958) + - redhat spec: add missing BuildRequires (#552) [Paride Legovini] + + -- Chad Smith Tue, 15 Sep 2020 20:19:10 -0600 + +cloud-init (20.3-2-g371b392c-0ubuntu1) groovy; urgency=medium + + * New upstream snapshot. + - util: remove debug statement (#556) [Joshua Powers] + - Fix cloud config on chef example (#551) [lucasmoura] + + -- James Falcon Wed, 26 Aug 2020 12:49:05 -0500 + +cloud-init (20.3-0ubuntu1) groovy; urgency=medium + + * New upstream release. + - Release 20.3 (#547) [James Falcon] (LP: #1892878) + - tox: bump the pylint version to 2.6.0 in the default run (#544) + [Paride Legovini] + - Azure: Add netplan driver filter when using hv_netvsc driver (#539) + [James Falcon] (LP: #1830740) + - query: do not handle non-decodable non-gzipped content (#543) + - DHCP sandboxing failing on noexec mounted /var/tmp (#521) [Eduardo Otubo] + - Update the list of valid ssh keys. (#487) + [Ole-Martin Bratteng] (LP: #1877869) + - cmd: cloud-init query to handle compressed userdata (#516) (LP: #1889938) + - Pushing cloud-init log to the KVP (#529) [Moustafa Moustafa] + - Add Alpine Linux support. (#535) [dermotbradley] + - Detect kernel version before swap file creation (#428) [Eduardo Otubo] + - cli: add devel make-mime subcommand (#518) + - user-data: only verify mime-types for TYPE_NEEDED and x-shellscript + (#511) (LP: #1888822) + - DataSourceOracle: retry twice (and document why we retry at all) (#536) + - Refactor Azure report ready code (#468) [Johnson Shi] + - tox.ini: pin correct version of httpretty in xenial{,-dev} envs (#531) + - Support Oracle IMDSv2 API (#528) [James Falcon] + - .travis.yml: run a doc build during CI (#534) + - doc/rtd/topics/datasources/ovf.rst: fix doc8 errors (#533) + - Fix 'Users and Groups' configuration documentation (#530) [sshedi] + - cloudinit.distros: update docstrings of add_user and create_user (#527) + - Fix headers for device types in network v2 docs (#532) + [Caleb Xavier Berger] + - Add AlexBaranowski as contributor (#508) [Aleksander Baranowski] + - DataSourceOracle: refactor to use only OPC v1 endpoint (#493) + - .github/workflows/stale.yml: s/Josh/Rick/ (#526) + - Fix a typo in apt pipelining module (#525) [Xiao Liang] + - test_util: parametrize devlist tests (#523) [James Falcon] + - Recognize LABEL_FATBOOT labels (#513) [James Falcon] (LP: #1841466) + - Handle additional identifier for SLES For HPC (#520) [Robert Schweikert] + - Revert "test-requirements.txt: pin pytest to <6 (#512)" (#515) + - test-requirements.txt: pin pytest to <6 (#512) + - Add "tsanghan" as contributor (#504) [tsanghan] + - fix brpm building (LP: #1886107) + - Adding eandersson as a contributor (#502) [Erik Olof Gunnar Andersson] + - azure: disable bouncing hostname when setting hostname fails (#494) + [Anh Vo] + - VMware: Support parsing DEFAULT-RUN-POST-CUST-SCRIPT (#441) + [xiaofengw-vmware] + - DataSourceAzure: Use ValueError when JSONDecodeError is not available + (#490) [Anh Vo] + - cc_ca_certs.py: fix blank line problem when removing CAs and adding + new one (#483) [dermotbradley] + - freebsd: py37-serial is now py37-pyserial (#492) [Gonéri Le Bouder] + - ssh exit with non-zero status on disabled user (#472) + [Eduardo Otubo] (LP: #1170059) + - cloudinit: remove global disable of pylint W0107 and fix errors (#489) + - networking: refactor wait_for_physdevs from cloudinit.net (#466) + (LP: #1884626) + - HACKING.rst: add pytest.param pytest gotcha (#481) + - cloudinit: remove global disable of pylint W0105 and fix errors (#480) + - Fix two minor warnings (#475) + - test_data: fix faulty patch (#476) + + -- James Falcon Tue, 25 Aug 2020 15:27:53 -0500 + +cloud-init (20.2-94-g3d06abc2-0ubuntu1) groovy; urgency=medium + + * New upstream snapshot. + - cc_mounts: handle missing fstab (#484) (LP: #1886531) + - LXD cloud_tests: support more lxd image formats (#482) [Paride Legovini] + - Add update_etc_hosts as default module on *BSD (#479) [Adam Dobrawy] + - cloudinit: fix tip-pylint failures and bump pinned pylint version (#478) + - Added BirknerAlex as contributor and sorted the file (#477) + [Alexander Birkner] + - Update list of types of modules in cli.rst [saurabhvartak1982] + - tests: use markers to configure disable_subp_usage (#473) + - Add mention of vendor-data to no-cloud format documentation (#470) + [Landon Kirk] + - Fix broken link to OpenStack metadata service docs (#467) + [Matt Riedemann] + - Disable ec2 mirror for non aws instances (#390) + [lucasmoura] (LP: #1456277) + - cloud_tests: don't pass --python-version to read-dependencies (#465) + - networking: refactor is_physical from cloudinit.net (#457) (LP: #1884619) + - Enable use of the caplog fixture in pytest tests, and add a + cc_final_message test using it (#461) + - RbxCloud: Add support for FreeBSD (#464) [Adam Dobrawy] + - Add schema for cc_chef module (#375) [lucasmoura] (LP: #1858888) + - test_util: add (partial) testing for util.mount_cb (#463) + - .travis.yml: revert to installing ubuntu-dev-tools (#460) + - HACKING.rst: add details of net refactor tracking (#456) + - .travis.yml: rationalise installation of dependencies in host (#449) + - Add dermotbradley as contributor. (#458) [dermotbradley] + - net/networking: remove unused functions/methods (#453) + - distros.networking: initial implementation of layout (#391) + - cloud-init.service.tmpl: use "rhel" instead of "redhat" (#452) + - Change from redhat to rhel in systemd generator tmpl (#450) + [Eduardo Otubo] + - Hetzner: support reading user-data that is base64 encoded. (#448) + [Scott Moser] (LP: #1884071) + - HACKING.rst: add strpath gotcha to testing gotchas section (#446) + - cc_final_message: don't create directories when writing boot-finished + (#445) (LP: #1883903) + - .travis.yml: only store new schroot if something has changed (#440) + - util: add ensure_dir_exists parameter to write_file (#443) + - printing the error stream of the dhclient process before killing it + (#369) [Moustafa Moustafa] + - Fix link to the MAAS documentation (#442) + [Paride Legovini] (LP: #1883666) + - RPM build: disable the dynamic mirror URLs when using a proxy (#437) + [Paride Legovini] + - util: rename write_file's copy_mode parameter to preserve_mode (#439) + - .travis.yml: use $TRAVIS_BUILD_DIR for lxd_image caching (#438) + - cli.rst: alphabetise devel subcommands and add net-convert to list (#430) + - Default to UTF-8 in /var/log/cloud-init.log (#427) [James Falcon] + - travis: cache the chroot we use for package builds (#429) + - test: fix all flake8 E126 errors (#425) [Joshua Powers] + - Fixes KeyError for bridge with no "parameters:" setting (#423) + [Brian Candler] (LP: #1879673) + - When tools.conf does not exist, running cmd "vmware-toolbox-cmd + config get deployPkg enable-custom-scripts", the return code will + be EX_UNAVAILABLE(69), on this condition, it should not take it as + error. (#413) [chengcheng-chcheng] + - Document CloudStack data-server well-known hostname (#399) [Gregor Riepl] + - test: move conftest.py to top-level, to cover tests/ also (#414) + - Replace cc_chef is_installed with use of subp.is_exe. (#421) + [Scott Moser] + - Move runparts to subp. (#420) [Scott Moser] + - Move subp into its own module. (#416) [Scott Moser] + - readme: point at travis-ci.com (#417) [Joshua Powers] + - New feature flag functionality and fix includes failing silently (#367) + [James Falcon] (LP: #1734939) + - Enhance poll imds logging (#365) [Moustafa Moustafa] + - test: fix all flake8 E121 and E123 errors (#404) [Joshua Powers] + + -- Daniel Watkins Fri, 10 Jul 2020 09:11:25 -0400 + +cloud-init (20.2-45-g5f7825e2-0ubuntu1) groovy; urgency=medium + + * d/control: drop python3-six, python3-unittest2, python3-pep8, + python3-pyflakes and python3-nose from Build-Depends. + * New upstream snapshot. + - test: fix all flake8 E241 (#403) [Joshua Powers] + - test: ignore flake8 E402 errors in main.py (#402) [Joshua Powers] + - cc_grub_dpkg: determine idevs in more robust manner with grub-probe + (#358) [Matthew Ruffell] (LP: #1877491) + - test: fix all flake8 E741 errors (#401) [Joshua Powers] + - tests: add groovy integration tests for ubuntu (#400) + - Enable chef_license support for chef infra client (#389) [Bipin Bachhao] + - testing: use flake8 again (#392) [Joshua Powers] + + -- Chad Smith Tue, 02 Jun 2020 09:51:50 -0600 + +cloud-init (20.2-38-g8377897b-0ubuntu1) groovy; urgency=medium + + * New upstream snapshot. + - enable Puppet, Chef mcollective in default config (#385) + [Mina Galić (deprecated: Igor Galić)] (LP: #1880279) + - HACKING.rst: introduce .net -> Networking refactor section (#384) + - Travis: do not install python3-contextlib2 (dropped dependency) (#388) + [Paride Legovini] + - HACKING: mention that .github-cla-signers is alpha-sorted (#380) + - Add bipinbachhao as contributor (#379) [Bipin Bachhao] + - cc_snap: validate that assertions property values are strings (#370) + - conftest: implement partial disable_subp_usage (#371) + - test_resolv_conf: refresh stale comment (#374) + + -- Chad Smith Wed, 27 May 2020 14:36:46 -0600 + +cloud-init (20.2-30-g8bcf1c06-0ubuntu1) groovy; urgency=medium + + * d/control: drop pyflakes as Build-Depends + - upstream uses python3-pyflakes now + * New upstream snapshot. + - cc_snap: apply validation to snap.commands properties (#364) + - make finding libc platform independent (#366) + [Mina Galić (deprecated: Igor Galić)] + - doc/rtd/topics/faq: Updates LXD docs links to current site (#368) [TomP] + - templater: drop Jinja Python 2 compatibility shim (#353) + - cloudinit: minor pylint fixes (#360) + - cloudinit: remove unneeded __future__ imports (#362) + - migrating momousta lp user to Moustafa-Moustafa GitHub user (#361) + [Moustafa Moustafa] + - cloud_tests: emit dots on Travis while fetching images (#347) + - Add schema to apt configure config (#357) [lucasmoura] (LP: #1858884) + - conftest: add docs and tests regarding CiTestCase's subp functionality + (#343) + + -- Chad Smith Mon, 18 May 2020 16:24:16 -0600 + +cloud-init (20.2-20-gd10ce3ec-0ubuntu1) groovy; urgency=medium + + * drop the following cherry-picks now included: + + cpick-6600c642-ec2-render-network-on-all-NICs-and-add-secondary-IPs-as + + cpick-986f37b0-cloudinit-move-to-pytest-for-running-tests-211 + + cpick-4fb6fd8a-net-ubuntu-focal-prioritize-netplan-over-eni-even-if + + cpick-04771d75-cc_disk_setup-fix-RuntimeError-270 + + cpick-c5e949c0-distros-tests-test_init-add-tests-for + + cpick-2566fdbe-net-introduce-is_ip_address-function-288 + + cpick-4f825b3e-cloudinit-refactor-util.is_ipv4-to-net.is_ipv4_address + + cpick-c478d0bf-distros-replace-invalid-characters-in-mirror-URLs-with + + cpick-1bbc4908-distros-drop-leading-trailing-hyphens-from-mirror-URL + + cpick-09fea85f-net-ignore-renderer-key-in-netplan-config-306 + + fix-cpick-4fb6fd8a-net-ubuntu-focal-prioritize-netplan-over-eni.patch + + cpick-9d7b35ce-cc_mounts-fix-incorrect-format-specifiers-316 + + cpick-0c5c7367-test_mounts-expand-happy-path-test-for-both-happy-paths + * New upstream snapshot. + - analyze/dump: refactor shared string into variable (#350) + - doc: update boot.rst with correct timing of runcmd (#351) + - HACKING.rst: change contact info to Rick Harding (#359) [lucasmoura] + - HACKING.rst: guide people to add themselves to the CLA file (#349) + - HACKING.rst: more unit testing documentation (#354) + - .travis.yml: don't run lintian during integration test package builds + (#352) + - Add test to ensure docs examples are valid cloud-init configs (#355) + [James Falcon] (LP: #1876414) + - make suse and sles support 127.0.1.1 (#336) [chengcheng-chcheng] + - Create tests to validate schema examples (#348) + [lucasmoura] (LP: #1876412) + - analyze/dump: add support for Amazon Linux 2 log lines (#346) + (LP: #1876323) + - bsd: upgrade support (#305) [Gonéri Le Bouder] + - Add lucasmoura as contributor (#345) [lucasmoura] + - Add "therealfalcon" as contributor (#344) [James Falcon] + - Adapt the package building scripts to use Python 3 (#231) + [Paride Legovini] + - DataSourceEc2: use metadata's NIC ordering to determine route-metrics + (#342) (LP: #1876312) + - .travis.yml: introduce caching (#329) + - cc_locale: introduce schema (#335) + - doc/rtd/conf.py: bump copyright year to 2020 (#341) + - yum_add_repo: Add Centos to the supported distro list (#340) + - Release 20.2 (#337) (LP: #1875951) + - doc/format: reference make-mime.py instead of an inline script (#334) + - Add docs about creating parent folders (#330) [Adrian Wilkins] + - DataSourceNoCloud/OVF: drop claim to support FTP (#333) (LP: #1875470) + - schema: ignore spurious pylint error (#332) + - schema: add json schema for write_files module (#152) + - BSD: find_devs_with_ refactoring (#298) [Gonéri Le Bouder] + - nocloud: drop work around for Linux 2.6 (#324) [Gonéri Le Bouder] + - cloudinit: drop dependencies on unittest2 and contextlib2 (#322) + - distros: handle a potential mirror filtering error case (#328) + - log: remove unnecessary import fallback logic (#327) + - .travis.yml: don't run integration test on ubuntu/* branches (#321) + - More unit test documentation (#314) + - conftest: introduce disable_subp_usage autouse fixture (#304) + - YAML align indent sizes for docs readability (#323) [Tak Nishigori] + - network_state: add missing space to log message (#325) + - tests: add missing mocks for get_interfaces_by_mac (#326) (LP: #1873910) + - test_mounts: expand happy path test for both happy paths (#319) + - cc_mounts: fix incorrect format specifiers (#316) (LP: #1872836) + - swap file "size" being used before checked if str (#315) [Eduardo Otubo] + - HACKING.rst: add pytest version gotchas section (#311) + - docs: Add steps to re-run cloud-id and cloud-init (#313) [Joshua Powers] + - readme: OpenBSD is now supported (#309) [Gonéri Le Bouder] + - net: ignore 'renderer' key in netplan config (#306) (LP: #1870421) + - Add support for NFS/EFS mounts (#300) [Andrew Beresford] (LP: #1870370) + - openbsd: set_passwd should not unlock user (#289) [Gonéri Le Bouder] + - tools/.github-cla-signers: add beezly as CLA signer (#301) + - util: remove unnecessary lru_cache import fallback (#299) + - HACKING.rst: reorganise/update CLA signature info (#297) + - distros: drop leading/trailing hyphens from mirror URL labels (#296) + - HACKING.rst: add note about variable annotations (#295) + - CiTestCase: stop using and remove sys_exit helper (#283) + - distros: replace invalid characters in mirror URLs with hyphens (#291) + (LP: #1868232) + - rbxcloud: gracefully handle arping errors (#262) [Adam Dobrawy] + - Fix cloud-init ignoring some misdeclared mimetypes in user-data. + [Kurt Garloff] + - net: ubuntu focal prioritize netplan over eni even if both present + (#267) (LP: #1867029) + - cloudinit: refactor util.is_ipv4 to net.is_ipv4_address (#292) + - net/cmdline: replace type comments with annotations (#294) + - HACKING.rst: add Type Annotations design section (#293) + - net: introduce is_ip_address function (#288) + - CiTestCase: remove now-unneeded parse_and_read helper method (#286) + - .travis.yml: allow 30 minutes of inactivity in cloud tests (#287) + - sources/tests/test_init: drop use of deprecated inspect.getargspec (#285) + - setup.py: drop NIH check_output implementation (#282) + - Identify SAP Converged Cloud as OpenStack [Silvio Knizek] + - add Openbsd support (#147) [Gonéri Le Bouder] + - HACKING.rst: add examples of the two test class types (#278) + - VMWware: support to update guest info gc status if enabled (#261) + [xiaofengw-vmware] + - Add lp-to-git mapping for kgarloff (#279) + - set_passwords: avoid chpasswd on BSD (#268) [Gonéri Le Bouder] + - HACKING.rst: add Unit Testing design section (#277) + - util: read_cc_from_cmdline handle urlencoded yaml content (#275) + - distros/tests/test_init: add tests for _get_package_mirror_info (#272) + - HACKING.rst: add links to new Code Review Process doc (#276) + - freebsd: ensure package update works (#273) [Gonéri Le Bouder] + - doc: introduce Code Review Process documentation (#160) + - tools: use python3 (#274) + - cc_disk_setup: fix RuntimeError (#270) (LP: #1868327) + - cc_apt_configure/util: combine search_for_mirror implementations (#271) + - bsd: boottime does not depend on the libc soname (#269) + [Gonéri Le Bouder] + - test_oracle,DataSourceOracle: sort imports (#266) + - DataSourceOracle: update .network_config docstring (#257) + - cloudinit/tests: remove unneeded with_logs configuration (#263) + - .travis.yml: drop stale comment (#255) + - .gitignore: add more common directories (#258) + - ec2: render network on all NICs and add secondary IPs as static (#114) + (LP: #1866930) + - ec2 json validation: fix the reference to the 'merged_cfg' key (#256) + [Paride Legovini] + - releases.yaml: quote the Ubuntu version numbers (#254) [Paride Legovini] + - cloudinit: remove six from packaging/tooling (#253) + - util/netbsd: drop six usage (#252) + - workflows: introduce stale pull request workflow (#125) + - cc_resolv_conf: introduce tests and stabilise output across Python + versions (#251) + - fix minor issue with resolv_conf template (#144) [andreaf74] + - doc: CloudInit also support NetBSD (#250) [Gonéri Le Bouder] + - Add Netbsd support (#62) [Gonéri Le Bouder] + - tox.ini: avoid substition syntax that causes a traceback on xenial (#245) + - Add pub_key_ed25519 to cc_phone_home (#237) [Daniel Hensby] + - Introduce and use of a list of GitHub usernames that have signed CLA + (#244) + - workflows/cla.yml: use correct username for CLA check (#243) + - tox.ini: use xenial version of jsonpatch in CI (#242) + - workflows: CLA validation altered to fail status on pull_request (#164) + - tox.ini: bump pyflakes version to 2.1.1 (#239) + - cloudinit: move to pytest for running tests (#211) + + -- Chad Smith Mon, 11 May 2020 20:17:06 -0600 + +cloud-init (20.1-10-g71af48df-0ubuntu5) focal; urgency=medium + + * cherry-pick 0c5c7367: test_mounts: expand happy path test for both + happy paths + + -- Daniel Watkins Thu, 16 Apr 2020 11:23:08 -0400 + +cloud-init (20.1-10-g71af48df-0ubuntu4) focal; urgency=medium + + * cherry-pick 9d7b35ce: cc_mounts: fix incorrect format specifiers + (#316) (LP: #1872836) + + -- Chad Smith Wed, 15 Apr 2020 15:09:04 -0600 + +cloud-init (20.1-10-g71af48df-0ubuntu3) focal; urgency=medium + + * d/patches: redact openbsd netbsd from tests until new-upstream-snapshot + - fix-cpick-4fb6fd8a-net-ubuntu-focal-prioritize-netplan-over-eni + * cherry-pick 6600c642: ec2: render network on all NICs and add + secondary IPs as (LP: #1866930) + * cherry-pick 986f37b0: cloudinit: move to pytest for running tests + (#211) + * cherry-pick 4fb6fd8a: net: ubuntu focal prioritize netplan over eni + even if both (LP: #1867029) + * cherry-pick 04771d75: cc_disk_setup: fix RuntimeError (#270) (LP: + #1868327) + * cherry-pick c5e949c0: distros/tests/test_init: add tests for + * cherry-pick 2566fdbe: net: introduce is_ip_address function (#288) + * cherry-pick 4f825b3e: cloudinit: refactor util.is_ipv4 to + net.is_ipv4_address + * cherry-pick c478d0bf: distros: replace invalid characters in mirror + URLs with (LP: #1868232) + * cherry-pick 1bbc4908: distros: drop leading/trailing hyphens from + mirror URL labels + * cherry-pick 09fea85f: net: ignore 'renderer' key in netplan config + (#306) (LP: #1870421) + + -- Chad Smith Fri, 03 Apr 2020 13:57:52 -0600 + +cloud-init (20.1-10-g71af48df-0ubuntu2) focal; urgency=medium + + * d/control: add python3-pytest to Build-Depends + - This fixes upstream daily builds. python3-nose is not removed from + Build-Depends because, currently, the Ubuntu package builds will still + use it for testing. + * cherry-pick 6600c642: ec2: render network on all NICs and add + secondary IPs as (LP: #1866930) + + -- Chad Smith Wed, 18 Mar 2020 14:01:37 -0600 + +cloud-init (20.1-10-g71af48df-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - instance-data: add cloud-init merged_cfg and sys_info keys to json + (#214) (LP: #1865969) + + -- Chad Smith Tue, 10 Mar 2020 11:14:53 -0600 + +cloud-init (20.1-9-g1f860e5a-0ubuntu1) focal; urgency=medium + + * New upstream snapshot: bug-fix-only during feature freeze + - ec2: Do not fallback to IMDSv1 on EC2 (#216) + [Frederick Lefebvre] (LP: #1866290) + - instance-data: write redacted cfg to instance-data.json (#233) + (LP: #1865947) + - net: support network-config:disabled on the kernel commandline (#232) + (LP: #1862702) + - ec2: only redact token request headers in logs, avoid altering request + (#230) (LP: #1865882) + + -- Chad Smith Fri, 06 Mar 2020 11:01:15 -0700 + +cloud-init (20.1-5-g67c8e53c-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - docs: typo fixed: dta → data [Alexey Vazhnov] + - Fixes typo on Amazon Web Services (#217) [Nick Wales] + - Fix docs for OpenStack DMI Asset Tag (#228) + [Mark T. Voelker] (LP: #1669875) + - Add physical network type: cascading to openstack helpers (#200) + [sab-systems] + - tests: add focal integration tests for ubuntu (#225) + + -- Chad Smith Thu, 27 Feb 2020 16:56:16 -0700 + +cloud-init (20.1-0ubuntu1) focal; urgency=medium + + * New upstream release. + - Release 20.1 (#222) (LP: #1863954) + - Update tooling for GitHub-based new releases (#223) + - ec2: Do not log IMDSv2 token values, instead use REDACTED (#219) + (LP: #1863943) + - utils: use SystemRandom when generating random password. (#204) + [Dimitri John Ledkov] + - docs: mount_default_files is a list of 6 items, not 7 (#212) + - azurecloud: fix issues with instances not starting (#205) (LP: #1861921) + - unittest: fix stderr leak in cc_set_password random unittest + output. (#208) + - cc_disk_setup: add swap filesystem force flag (#207) + - import sysvinit patches from freebsd-ports tree (#161) [Igor Galić] + - docs: fix typo (#195) [Edwin Kofler] + + -- Daniel Watkins Thu, 20 Feb 2020 16:36:44 -0500 + +cloud-init (19.4-56-g06e324ff-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - sysconfig: distro-specific config rendering for BOOTPROTO option (#162) + [Robert Schweikert] (LP: #1800854) + - cloudinit: replace "from six import X" imports (except in util.py) (#183) + - run-container: use 'test -n' instead of 'test ! -z' (#202) + [Paride Legovini] + - net/cmdline: correctly handle static ip= config (#201) + [Dimitri John Ledkov] (LP: #1861412) + - Replace mock library with unittest.mock (#186) + - HACKING.rst: update CLA link (#199) + - Scaleway: Fix DatasourceScaleway to avoid backtrace (#128) + [Louis Bouchard] + - cloudinit/cmd/devel/net_convert.py: add missing space (#191) + - tools/run-container: drop support for python2 (#192) [Paride Legovini] + - Print ssh key fingerprints using sha256 hash (#188) (LP: #1860789) + - Make the RPM build use Python 3 (#190) [Paride Legovini] + - cc_set_password: increase random pwlength from 9 to 20 (#189) + (LP: #1860795) (CVE-2020-8632) + - .travis.yml: use correct Python version for xenial tests (#185) + - cloudinit: remove ImportError handling for mock imports (#182) + - Do not use fallocate in swap file creation on xfs. (#70) + [Eduardo Otubo] (LP: #1781781) + - .readthedocs.yaml: install cloud-init when building docs (#181) + (LP: #1860450) + - Introduce an RTD config file, and pin the Sphinx version to the RTD + default (#180) + - Drop most of the remaining use of six (#179) + - Start removing dependency on six (#178) + - Add Rootbox & HyperOne to list of cloud in README (#176) [Adam Dobrawy] + - docs: add proposed SRU testing procedure (#167) + - util: rename get_architecture to get_dpkg_architecture (#173) + - Ensure util.get_architecture() runs only once (#172) + + -- Chad Smith Wed, 05 Feb 2020 13:56:17 -0700 + +cloud-init (19.4-33-gbb4131a2-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - Only use gpart if it is the BSD gpart (#131) [Conrad Hoffmann] + - freebsd: remove superfluous exception mapping (#166) [Gonéri Le Bouder] + + -- Chad Smith Tue, 14 Jan 2020 14:02:06 -0700 + +cloud-init (19.4-31-g3f6192b3-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - ssh_auth_key_fingerprints_disable test: fix capitalization (#165) + [Paride Legovini] + - util: move uptime's else branch into its own boottime function (#53) + [Igor Galić] (LP: #1853160) + - net: fix rendering of 'static6' in network config (#77) (LP: #1850988) + - fixed minor bug with mkswap in cc_disk_setup.py (#143) [andreaf74] + - freebsd: fix create_group() cmd (#146) [Gonéri Le Bouder] + + -- Chad Smith Mon, 13 Jan 2020 20:21:09 -0700 + +cloud-init (19.4-16-gf8950d63-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - Add support for the amazon variant in cloud.cfg.tmpl (#119) + [Frederick Lefebvre] + - ci: remove Python 2.7 from CI runs (#137) + - modules: drop cc_snap_config config module (#134) + - migrate-lp-user-to-github: ensure Launchpad repo exists (#136) + - docs: add initial troubleshooting to FAQ (#104) [Joshua Powers] + - doc: update cc_set_hostname frequency and descrip (#109) + [Joshua Powers] (LP: #1827021) + - freebsd: introduce the freebsd renderer (#61) [Gonéri Le Bouder] + - cc_snappy: remove deprecated module (#127) + - HACKING.rst: clarify that everyone needs to do the LP->GH dance (#130) + - freebsd: cloudinit service requires devd (#132) [Gonéri Le Bouder] + - cloud-init: fix capitalisation of SSH (#126) + - doc: update cc_ssh clarify host and auth keys + [Joshua Powers] (LP: #1827021) + + -- Chad Smith Mon, 06 Jan 2020 09:52:22 -0700 + +cloud-init (19.4-1-g8c96cbc1-0ubuntu1) focal; urgency=medium + + * debian/cloud-init.templates: enable RbxCloud: HypeOne and Rootbox detection + by default + * New upstream snapshot. + - ci: emit names of tests run in Travis (#120) + - Release 19.4 (LP: #1856761) + - rbxcloud: fix dsname in RbxCloud [Adam Dobrawy] (LP: #1855196) + - tests: Add tests for value of dsname in datasources [Adam Dobrawy] + - apport: Add RbxCloud ds [Adam Dobrawy] + - docs: Updating index of datasources [Adam Dobrawy] + - docs: Fix anchor of datasource_rbx [Adam Dobrawy] + - settings: Add RbxCloud [Adam Dobrawy] + - doc: specify _ over - in cloud config modules + [Joshua Powers] (LP: #1293254) + - tools: Detect python to use via env in migrate-lp-user-to-github + [Adam Dobrawy] + - Partially revert "fix unlocking method on FreeBSD" (#116) + - tests: mock uid when running as root (#113) + [Joshua Powers] (LP: #1856096) + - cloudinit/netinfo: remove unused getgateway (#111) + - docs: clear up apt config sections (#107) [Joshua Powers] (LP: #1832823) + - doc: add kernel command line option to user data (#105) + [Joshua Powers] (LP: #1846524) + - config/cloud.cfg.d: update README [Joshua Powers] (LP: #1855006) + + -- Chad Smith Wed, 18 Dec 2019 11:02:04 -0700 + +cloud-init (19.3-74-g129b1c4e-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - azure: avoid re-running cloud-init when instance-id is byte-swapped + (#84) [AOhassan] + - fix unlocking method on FreeBSD [Igor Galić] (LP: #1854594) + - debian: add reference to the manpages [Joshua Powers] + - ds_identify: if /sys is not available use dmidecode (#42) + [Igor Galić] (LP: #1852442) + - docs: add cloud-id manpage [Joshua Powers] + - docs: add cloud-init-per manpage [Joshua Powers] + - docs: add cloud-init manpage [Joshua Powers] + - docs: add additional details to per-instance/once [Joshua Powers] + - Merge pull request #96 from fred-lefebvre/master [Joshua Powers] + - Update doc-requirements.txt [Joshua Powers] + - doc-requirements: add missing dep [Joshua Powers] + - Merge pull request #95 from powersj/docs/bugs [Joshua Powers] + - dhcp: Support RedHat dhcp rfc3442 lease format for option 121 (#76) + [Eric Lafontaine] (LP: #1850642) + - network_state: handle empty v1 config (#45) (LP: #1852496) + - Merge pull request #94 from gaughen/patch-1 [Joshua Powers] + - docs: fix line length and remove highlighting [Joshua Powers] + - docs: Add security.md to readthedocs [Joshua Powers] + - Multiple file fix for AuthorizedKeysFile config (#60) [Eduardo Otubo] + - Merge pull request #88 from OddBloke/travis [Joshua Powers] + - doc: update links on README.md [Joshua Powers] + - doc: Updates to wording of README.md [Joshua Powers] + - Add security.md [Joshua Powers] + - setup.py: Amazon Linux sets libexec to /usr/libexec (#52) + [Frederick Lefebvre] + + -- Chad Smith Thu, 12 Dec 2019 16:05:35 -0700 + +cloud-init (19.3-41-gc4735dd3-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - Fix linting failure in test_url_helper (#83) [Eric Lafontaine] + + -- Chad Smith Tue, 03 Dec 2019 14:46:25 -0700 + +cloud-init (19.3-40-gf69d33a7-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - url_helper: read_file_or_url should pass headers param into readurl + (#66) (LP: #1854084) + - lp-to-git-users: adding elafontaine (#74) [Eric Lafontaine] + - lp-to-git-users: adding xiaofengw-vmware [Xiaofeng Wang] + - dmidecode: log result *after* stripping [Igor Galić] + - cloud_tests: add azure platform support to integration tests + [ahosmanmsft] + - set_passwords: support for FreeBSD (#46) [Igor Galić] + - tools: migrate-lp-user-to-github removes repo_dir if created (#35) + - Correct jumbled documentation for cc_set_hostname module (#64) + [do3meli] (LP: #1853543) + - FreeBSD: fix for get_linux_distro() and lru_cache (#59) + [Igor Galić] (LP: #1815030) + - lp-to-git-users: adding do3meli [Dominic Schlegel] + + -- Chad Smith Mon, 02 Dec 2019 16:26:24 -0700 + +cloud-init (19.3-30-g4bc399e0-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - ec2: Add support for AWS IMDS v2 (session-oriented) (#55) + - tests: Fix cloudsigma tests when no dmidecode data is present. (#57) + [Scott Moser] + - net: IPv6, accept_ra, slaac, stateless (#51) + [Harald] (LP: #1806014, #1808647) + - docs: Update the configdrive datasource links (#44) + [Joshua Powers] (LP: #1852461) + - lp-to-git-users: adding PengpengSun [Pengpeng Sun] + - lp-to-git-users: adding trstringer [Thomas Stringer] + - lp-to-git-users: adding hjensas [Harald Jensås] + - distro: correctly set usr_lib_exec path for FreeBSD distro (#40) + [Igor Galić] (LP: #1852491) + - azure: support secondary ipv6 addresses (#33) + - Fix metadata check when local-hostname is null (#32) + [Mark Goddard] (LP: #1852100) + - Merge pull request #38 from blackboxsw/cleanup/run-ci-on-pull-request + - switch default FreeBSD salt minion pkg from py27 to py36 + [Dominic Schlegel] + - lp-to-git-users: adding igalic [Igor Galić] + - add data-server dns entry as new metadata server detection + [Joshua Hügli] + - lp-to-git-users: adding chrisglass [Chris Glass] + - lp-to-git-users: adding larsks [Lars Kellogg-Stedman] + - Merge pull request #31 from raharper/fix/pycodestyle-tip + - reporting: Using a uuid to enforce uniqueness on the KVP keys. [momousta] + - lp-to-git-users: adding paride [Paride Legovini] + - lp-to-git-users: adding powersj [Joshua Powers] + - lp-to-git-users: adding blackboxsw + - lp-to-git-users: adding raharper + - docs: touchups in rtd intro and README.md + - doc: update launchpad git refs to github + - tools: migrate script needs to write tools/.lp-to-git-user + - github: drop pull-request template to prepare for migration + - tools: add migrate-lp-user-to-github script to link LP to github + - github: new basic project readme + + -- Chad Smith Fri, 22 Nov 2019 20:09:10 -0700 + +cloud-init (19.3-0ubuntu1) focal; urgency=medium + + * New upstream release. + - Release 19.3 (LP: #1851428) + - azure: support matching dhcp route-metrics for dual-stack ipv4 ipv6 + (LP: #1850308) + - configdrive: fix subplatform config-drive for /config-drive source + [David Kindred] (LP: #1849731) + + -- Chad Smith Tue, 05 Nov 2019 14:50:06 -0700 + +cloud-init (19.2-78-ge8138959-0ubuntu1) focal; urgency=medium + + * New upstream snapshot. + - DataSourceSmartOS: reconfigure network on each boot + [Mike Gerdts] (LP: #1765801) + - Add config for ssh-key import and consuming user-data [Pavel Zakharov] + - net: fix subnet_is_ipv6() for stateless|stateful + [Harald Jensås] (LP: #1848690) + - OVF: disable custom script execution by default [Xiaofeng Wang] + - cc_puppet: Implement csr_attributes.yaml support [Matthias Baur] + - cloud-init.service: on centos/fedora/redhat wait on + NetworkManager.service (LP: #1843334) + - azure: Do not lock user on instance id change + [Sam Eiderman] (LP: #1849677) + - net/netplan: use ipv6-mtu key for specifying ipv6 mtu values + - Fix usages of yaml, and move yaml_dump to safeyaml.dumps. + [Scott Moser] (LP: #1849640) + - exoscale: Increase url_max_wait to 120s. [Chris Glass] + - net/sysconfig: fix available check on SUSE distros + [Robert Schweikert] (LP: #1849378) + - docs: Fix incorrect Azure IMDS IP address [Joshua Powers] (LP: #1849508) + - introduce .travis.yml + - net: enable infiniband support in eni and sysconfig renderers + [Darren Birkett] (LP: #1847114) + - guestcust_util: handle special characters in config file [Xiaofeng Wang] + - fix some more typos in comments [Dominic Schlegel] + - replace any deprecated log.warn with log.warning + [Dominic Schlegel] (LP: #1508442) + - net: handle openstack dhcpv6-stateless configuration + [Harald Jensås] (LP: #1847517) + - Add .venv/ to .gitignore [Dominic Schlegel] + - Small typo fixes in code comments. [Dominic Schlegel] + - cloud_test/lxd: Retry container delete a few times + - Add Support for e24cloud to Ec2 datasource. [Scott Moser] (LP: #1696476) + - Add RbxCloud datasource [Adam Dobrawy] + - get_interfaces: don't exclude bridge and bond members (LP: #1846535) + - Add support for Arch Linux in render-cloudcfg [Conrad Hoffmann] + - util: json.dumps on python 2.7 will handle UnicodeDecodeError on binary + (LP: #1801364) + - debian/ubuntu: add missing word to netplan/ENI header (LP: #1845669) + - ovf: do not generate random instance-id for IMC customization path + - sysconfig: only write resolv.conf if network_state has DNS values + (LP: #1843634) + - sysconfig: use distro variant to check if available (LP: #1843584) + - systemd/cloud-init.service.tmpl: start after wicked.service + [Robert Schweikert] + - docs: fix zstack documentation lints + - analyze/show: remove trailing space in output + - Add missing space in warning: "not avalid seed" [Brian Candler] + - pylintrc: add 'enter_context' to generated-members list + - Add datasource for ZStack platform. [Shixin Ruan] (LP: #1841181) + - docs: organize TOC and update summary of project [Joshua Powers] + - tools: make clean now cleans the dev directory, not the system + - docs: create cli specific page [Joshua Powers] + - docs: added output examples to analyze.rst [Joshua Powers] + - docs: doc8 fixes for instancedata page [Joshua Powers] + - docs: clean up formatting, organize boot page [Joshua Powers] + + -- Ryan Harper Fri, 01 Nov 2019 11:01:15 -0500 + +cloud-init (19.2-36-g059d049c-0ubuntu3) eoan; urgency=medium + + * debian/cloud-init.config get_yaml_list should return only the value + (LP: #1846511) + + -- Ryan Harper Wed, 09 Oct 2019 16:32:17 -0500 + +cloud-init (19.2-36-g059d049c-0ubuntu2) eoan; urgency=medium + + * cherry-pick a7d8d032: get_interfaces: don't exclude bridge and bond + members (LP: #1846535) + + -- Daniel Watkins Fri, 04 Oct 2019 11:42:12 -0400 + +cloud-init (19.2-36-g059d049c-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - net: add is_master check for filtering device list (LP: #1844191) + - docs: more complete list of availability [Joshua Powers] + - docs: start FAQ page [Joshua Powers] + - docs: cleanup output & order of datasource page [Joshua Powers] + - Brightbox: restrict detection to require full domain match + .brightbox.com [Scott Moser] + - VMWware: add option into VMTools config to enable/disable custom script. + [Xiaofeng Wang] + - net,Oracle: Add support for netfailover detection + - atomic_helper: add DEBUG logging to write_file (LP: #1843276) + - doc: document doc, create makefile and tox target [Joshua Powers] + - .gitignore: ignore files produced by package builds + - docs: fix whitespace, spelling, and line length [Joshua Powers] + - docs: remove unnecessary file in doc directory [Joshua Powers] + + -- Daniel Watkins Tue, 17 Sep 2019 12:12:27 +0200 + +cloud-init (19.2-24-ge7881d5c-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - Oracle: Render secondary vnic IP and MTU values only + - exoscale: fix sysconfig cloud_config_modules overrides (LP: #1841454) + - net/cmdline: refactor to allow multiple initramfs network config sources + + -- Chad Smith Wed, 28 Aug 2019 14:44:36 -0600 + +cloud-init (19.2-21-ge6383719-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - ubuntu-drivers: call db_x_loadtemplatefile to accept NVIDIA EULA + (LP: #1840080) + - Add missing #cloud-config comment on first example in documentation. + [Florian Müller] + - ubuntu-drivers: emit latelink=true debconf to accept nvidia eula + (LP: #1840080) + - DataSourceOracle: prefer DS network config over initramfs + - format.rst: add text/jinja2 to list of content types (+ cleanups) + - Add GitHub pull request template to point people at hacking doc + - cloudinit/distros/parsers/sys_conf: add docstring to SysConf + - pyflakes: remove unused variable [Joshua Powers] + + -- Chad Smith Thu, 22 Aug 2019 11:15:08 -0600 + +cloud-init (19.2-13-g2f3bb764-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - Azure: Record boot timestamps, system information, and diagnostic events + [Anh Vo] + - DataSourceOracle: configure secondary NICs on Virtual Machines + - distros: fix confusing variable names + - azure/net: generate_fallback_nic emits network v2 config instead of v1 + + -- Chad Smith Thu, 15 Aug 2019 09:42:20 -0600 + +cloud-init (19.2-9-g15584720-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - Add support for publishing host keys to GCE guest attributes + [Rick Wright] + - New data source for the Exoscale.com cloud platform [Chris Glass] + - doc: remove intersphinx extension + - cc_set_passwords: rewrite documentation (LP: #1838794) + * d/cloud-init.templates: add Exoscale data source + + -- Daniel Watkins Fri, 09 Aug 2019 13:57:28 -0400 + +cloud-init (19.2-5-g496aaa94-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - net/cmdline: split interfaces_by_mac and init network config + determination + - stages: allow data sources to override network config source order + - cloud_tests: updates and fixes + - Fix bug rendering MTU on bond or vlan when input was netplan. + [Scott Moser] (LP: #1836949) + - net: update net sequence, include wait on netdevs, opensuse netrules + path (LP: #1817368) + + -- Chad Smith Tue, 06 Aug 2019 13:56:18 -0600 + +cloud-init (19.2-0ubuntu1) eoan; urgency=medium + + * New upstream release. + - Release 19.2 (LP: #1836921) + - net: add rfc3442 (classless static routes) to EphemeralDHCP + (LP: #1821102) + - templates/ntp.conf.debian.tmpl: fix missing newline for pools + (LP: #1836598) + - Support netplan renderer in Arch Linux [Conrad Hoffmann] + - Fix typo in publicly viewable documentation. [David Medberry] + - Add a cdrom size checker for OVF ds to ds-identify + [Pengpeng Sun] (LP: #1806701) + - VMWare: Trigger the post customization script via cc_scripts module. + [Xiaofeng Wang] (LP: #1833192) + - Cloud-init analyze module: Added ability to analyze boot events. + [Sam Gilson] + - Update debian eni network configuration location, retain Ubuntu setting + [Janos Lenart] + - net: skip bond interfaces in get_interfaces + [Stanislav Makar] (LP: #1812857) + - Fix a couple of issues raised by a coverity scan + - Add missing dsname for Hetzner Cloud datasource [Markus Schade] + - doc: indicate that netplan is default in Ubuntu now + - azure: add region and AZ properties from imds compute location metadata + - sysconfig: support more bonding options [Penghui Liao] + - cloud-init-generator: use libexec path to ds-identify on redhat systems + (LP: #1833264) + - tools/build-on-freebsd: update to python3 [Gonéri Le Bouder] + - Allow identification of OpenStack by Asset Tag + [Mark T. Voelker] (LP: #1669875) + - Fix spelling error making 'an Ubuntu' consistent. [Brian Murray] + - run-container: centos: comment out the repo mirrorlist [Paride Legovini] + - netplan: update netplan key mappings for gratuitous-arp (LP: #1827238) + - freebsd: fix the name of cloudcfg VARIANT [Gonéri Le Bouder] + - freebsd: ability to grow root file system [Gonéri Le Bouder] + - freebsd: NoCloud data source support [Gonéri Le Bouder] (LP: #1645824) + + -- Ryan Harper Wed, 17 Jul 2019 12:43:15 -0500 + +cloud-init (19.1-1-gbaa47854-0ubuntu1) eoan; urgency=medium + + * New upstream snapshot. + - Azure: Return static fallback address as if failed to find endpoint + [Jason Zions (MSFT)] + + -- Chad Smith Fri, 10 May 2019 15:34:21 -0600 + +cloud-init (19.1-0ubuntu1) eoan; urgency=medium + + * New upstream release. + - release 19.1 (LP: #1828479) + - freebsd: add chpasswd pkg in the image [Gonéri Le Bouder] + - tests: add Eoan release [Paride Legovini] + - cc_mounts: check if mount -a on no-change fstab path + [Jason Zions (MSFT)] (LP: #1825596) + - replace remaining occurrences of LOG.warn + - DataSourceAzure: Adjust timeout for polling IMDS [Anh Vo] + - Azure: Changes to the Hyper-V KVP Reporter [Anh Vo] + - git tests: no longer show warning about safe yaml. [Scott Moser] + - tools/read-version: handle errors [Chad Miller] + - net/sysconfig: only indicate available on known sysconfig distros + (LP: #1819994) + - packages: update rpm specs for new bash completion path (LP: #1825444) + - test_azure: mock util.SeLinuxGuard where needed + [Jason Zions (MSFT)] (LP: #1825253) + - setup.py: install bash completion script in new location + - mount_cb: do not pass sync and rw options to mount + [Gonéri Le Bouder] (LP: #1645824) + - cc_apt_configure: fix typo in apt documentation [Dominic Schlegel] + + -- Chad Smith Fri, 10 May 2019 12:11:06 -0600 + +cloud-init (18.5-62-g6322c2dd-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - Revert "DataSource: move update_events from a class to an instance..." + + -- Daniel Watkins Wed, 10 Apr 2019 16:23:25 -0400 + +cloud-init (18.5-61-gb76714c3-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - Change DataSourceNoCloud to ignore file system label's case. + [Risto Oikarinen] + - cmd:main.py: Fix missing 'modules-init' key in modes dict + [Antonio Romito] (LP: #1815109) + - ubuntu_advantage: rewrite cloud-config module + - Azure: Treat _unset network configuration as if it were absent + [Jason Zions (MSFT)] (LP: #1823084) + - DatasourceAzure: add additional logging for azure datasource [Anh Vo] + - cloud_tests: fix apt_pipelining test-cases + - Azure: Ensure platform random_seed is always serializable as JSON. + [Jason Zions (MSFT)] + - net/sysconfig: write out SUSE-compatible IPv6 config [Robert Schweikert] + - tox: Update testenv for openSUSE Leap to 15.0 [Thomas Bechtold] + - net: Fix ipv6 static routes when using eni renderer + [Raphael Glon] (LP: #1818669) + - Add ubuntu_drivers config module + - doc: Refresh Azure walinuxagent docs + - tox: bump pylint version to latest (2.3.1) + - DataSource: move update_events from a class to an instance attribute + (LP: #1819913) + - net/sysconfig: Handle default route setup for dhcp configured NICs + [Robert Schweikert] (LP: #1812117) + - DataSourceEc2: update RELEASE_BLOCKER to be more accurate + + -- Daniel Watkins Wed, 10 Apr 2019 11:49:03 -0400 + +cloud-init (18.5-45-g3554ffe8-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - cloud-init-per: POSIX sh does not support string subst, use sed + (LP: #1819222) + + -- Daniel Watkins Fri, 08 Mar 2019 17:42:34 -0500 + +cloud-init (18.5-44-g7c07af28-0ubuntu2) disco; urgency=medium + + * d/postinst: remove now-incorrect apt pipelining configuration + + -- Daniel Watkins Fri, 08 Mar 2019 09:56:42 -0500 + +cloud-init (18.5-44-g7c07af28-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - Support locking user with usermod if passwd is not available. + [Scott Moser] + - Example for Microsoft Azure data disk added. [Anton Olifir] + - clean: correctly determine the path for excluding seed directory + (LP: #1818571) + - helpers/openstack: Treat unknown link types as physical (LP: #1639263) + - drop Python 2.6 support and our NIH version detection + - tip-pylint: Fix assignment-from-return-none errors + - net: append type:dhcp[46] only if dhcp[46] is True in v2 netconfig + [Kurt Stieger] (LP: #1818032) + - cc_apt_pipelining: stop disabling pipelining by default (LP: #1794982) + - tests: fix some slow tests and some leaking state + - util: don't determine string_types ourselves + - cc_rsyslog: Escape possible nested set (LP: #1816967) + - Enable encrypted_data_bag_secret support for Chef + [Eric Williams] (LP: #1817082) + - azure: Filter list of ssh keys pulled from fabric [Jason Zions (MSFT)] + - doc: update merging doc with fixes and some additional details/examples + - tests: integration test failure summary to use traceback if empty error + - This is to fix https://bugs.launchpad.net/cloud-init/+bug/1812676 + [Vitaly Kuznetsov] + - EC2: Rewrite network config on AWS Classic instances every boot + [Guilherme G. Piccoli] (LP: #1802073) + - netinfo: Adjust ifconfig output parsing for FreeBSD ipv6 entries + (LP: #1779672) + - netplan: Don't render yaml aliases when dumping netplan (LP: #1815051) + - add PyCharm IDE .idea/ path to .gitignore [Dominic Schlegel] + - correct grammar issue in instance metadata documentation + [Dominic Schlegel] (LP: #1802188) + - clean: cloud-init clean should not trace when run from within cloud_dir + (LP: #1795508) + - Resolve flake8 comparison and pycodestyle over-ident issues + [Paride Legovini] + * Update netplan dependency package (LP: #1813667) + * Fix build-depends-on-obsolete-package for dh-systemd + * Change Priority from extra to optional + * Override lintian warnings about WantedBy=cloud-init.target + * Change Maintainer to Ubuntu Developers + + -- Daniel Watkins Thu, 07 Mar 2019 10:32:26 -0500 + +cloud-init (18.5-21-g8ee294d5-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - opennebula: also exclude epochseconds from changed environment vars + (LP: #1813641) + - systemd: Render generator from template to account for system + differences. [Robert Schweikert] + - sysconfig: On SUSE, use STARTMODE instead of ONBOOT + [Robert Schweikert] (LP: #1799540) + - flake8: use ==/!= to compare str, bytes, and int literals + [Paride Legovini] + + -- Chad Smith Mon, 28 Jan 2019 17:01:53 -0700 + +cloud-init (18.5-17-gd1a2fe73-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - opennebula: exclude EPOCHREALTIME as known bash env variable with a + delta (LP: #1813383) + - tox: fix disco httpretty dependencies for py37 (LP: #1813361) + - run-container: uncomment baseurl in yum.repos.d/*.repo when using a + proxy [Paride Legovini] + - lxd: install zfs-linux instead of zfs meta package + [Johnson Shi] (LP: #1799779) + - net/sysconfig: do not write a resolv.conf file with only the header. + [Robert Schweikert] + - net: Make sysconfig renderer compatible with Network Manager. + [Eduardo Otubo] + - cc_set_passwords: Fix regex when parsing hashed passwords + [Marlin Cremers] (LP: #1811446) + - net: Wait for dhclient to daemonize before reading lease file + [Jason Zions] (LP: #1794399) + - [Azure] Increase retries when talking to Wireserver during metadata walk + [Jason Zions] + - Add documentation on adding a datasource. + - doc: clean up some datasource documentation. + - ds-identify: fix wrong variable name in ovf_vmware_transport_guestinfo. + - Scaleway: Support ssh keys provided inside an instance tag. [PORTE Loïc] + - OVF: simplify expected return values of transport functions. + - Vmware: Add support for the com.vmware.guestInfo OVF transport. + (LP: #1807466) + - HACKING.rst: change contact info to Josh Powers + + -- Chad Smith Sat, 26 Jan 2019 08:27:35 -0700 + +cloud-init (18.5-1-g5b065316-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - Update to pylint 2.2.2. + - Release 18.5 (LP: #1808380) + - tests: add Disco release [Joshua Powers] + - net: render 'metric' values in per-subnet routes (LP: #1805871) + - write_files: add support for appending to files. [James Baxter] + - config: On ubuntu select cloud archive mirrors for armel, armhf, arm64. + (LP: #1805854) + - dhclient-hook: cleanups, tests and fix a bug on 'down' event. + - NoCloud: Allow top level 'network' key in network-config. (LP: #1798117) + - ovf: Fix ovf network config generation gateway/routes (LP: #1806103) + + -- Ryan Harper Fri, 14 Dec 2018 14:45:46 -0600 + +cloud-init (18.4-31-gbf791715-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - azure: detect vnet migration via netlink media change event + [Tamilmani Manoharan] + - Azure: fix copy/paste error in error handling when reading azure ovf. + [Adam DePue] + - tests: fix incorrect order of mocks in test_handle_zfs_root. + - doc: Change dns_nameserver property to dns_nameservers. [Tomer Cohen] + - OVF: identify label iso9660 filesystems with label 'OVF ENV'. + - logs: collect-logs ignore instance-data-sensitive.json on non-root user + (LP: #1805201) + - net: Ephemeral*Network: add connectivity check via URL + [Igor Galić] + + -- Chad Smith Thu, 29 Nov 2018 16:23:24 -0700 + +cloud-init (18.4-24-g8f812a15-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - azure: _poll_imds only retry on 404. Fail on Timeout (LP: #1803598) + - resizefs: Prefix discovered devpath with '/dev/' when path does not + exist [Igor Galić] + + -- Chad Smith Thu, 15 Nov 2018 16:11:20 -0700 + +cloud-init (18.4-22-g6062595b-0ubuntu1) disco; urgency=medium + + * New upstream snapshot. + - azure: retry imds polling on requests.Timeout (LP: #1800223) + - azure: Accept variation in error msg from mount for ntfs volumes + [Jason Zions] (LP: #1799338) + - azure: fix regression introduced when persisting ephemeral dhcp lease + [Aswin Rajamannar] + - azure: add udev rules to create cloud-init Gen2 disk name symlinks + (LP: #1797480) + - tests: ec2 mock missing httpretty user-data and instance-identity routes + - azure: remove /etc/netplan/90-hotplug-azure.yaml when net from IMDS + - azure: report ready to fabric after reprovision and reduce logging + [Aswin Rajamannar] (LP: #1799594) + - query: better error when missing read permission on instance-data + - instance-data: fallback to instance-data.json if sensitive is absent. + (LP: #1798189) + - docs: remove colon from network v1 config example. [Tomer Cohen] + - Add cloud-id binary to packages for SUSE [Jason Zions] + - systemd: On SUSE ensure cloud-init.service runs before wicked + [Robert Schweikert] (LP: #1799709) + - update detection of openSUSE variants [Robert Schweikert] + - azure: Add apply_network_config option to disable network from IMDS + (LP: #1798424) + - Correct spelling in an error message (udevadm). [Katie McLaughlin] + + -- Chad Smith Mon, 12 Nov 2018 20:33:12 -0700 + +cloud-init (18.4-7-g4652b196-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - tests: meta_data key changed to meta-data in ec2 instance-data.json + (LP: #1797231) + - tests: fix kvm integration test to assert flexible config-disk path + (LP: #1797199) + + -- Chad Smith Thu, 11 Oct 2018 09:24:46 -0600 + +cloud-init (18.4-5-g6ee8a2c5-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - tools: Add cloud-id command line utility + - instance-data: Add standard keys platform and subplatform. Refactor ec2. + - net: ignore nics that have "zero" mac address. (LP: #1796917) + - tests: fix apt_configure_primary to be more flexible + - Ubuntu: update sources.list to comment out deb-src entries. (LP: #74747) + + -- Chad Smith Tue, 09 Oct 2018 16:56:35 -0600 + +cloud-init (18.4-0ubuntu1) cosmic; urgency=medium + + * New upstream release. + - release 18.4 + - tests: allow skipping an entire cloud_test without running. + - tests: disable lxd tests on cosmic + - cii-tests: use unittest2.SkipTest in ntp_chrony due to new deps + - lxd: adjust to snap installed lxd. + - docs: surface experimental doc in instance-data.json + - tests: fix ec2 integration tests. process meta_data instead of meta-data + - Add support for Infiniband network interfaces (IPoIB). [Mark Goddard] + - cli: add cloud-init query subcommand to query instance metadata + - tools/tox-venv: update for new features. + - pylint: ignore warning assignment-from-no-return for _write_network + - stages: Fix bug causing datasource to have incorrect sys_cfg. + - Remove dead-code _write_network distro implementations. + - net_util: ensure static configs have netmask in translate_network result + [Thomas Berger] + - Fall back to root:root on syslog permissions if other options fail. + [Robert Schweikert] + - tests: Add mock for util.get_hostname. [Robert Schweikert] + + -- Chad Smith Tue, 02 Oct 2018 19:55:23 -0600 + +cloud-init (18.3-46-gbb60f61b-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - ds-identify: doc string cleanup. + - OpenStack: Support setting mac address on bond. [Fabian Wiesel] + + -- Ryan Harper Fri, 14 Sep 2018 16:13:19 -0500 + +cloud-init (18.3-44-g84bf2482-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - bash_completion/cloud-init: fix shell syntax error. + - EphemeralIPv4Network: Be more explicit when adding default route. + - OpenStack: support reading of newer versions of metdata. + - OpenStack: fix bug causing 'latest' version to be used from network. + - user-data: jinja template to render instance-data.json in cloud-config + + -- Ryan Harper Fri, 14 Sep 2018 14:06:29 -0500 + +cloud-init (18.3-39-g757247f9-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - config: disable ssh access to a configured user account + + -- Ryan Harper Mon, 10 Sep 2018 17:11:51 -0500 + +cloud-init (18.3-38-gd47d404e-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - tests: print failed testname instead of docstring upon failure + - tests: Disallow use of util.subp except for where needed. + - sysconfig: refactor sysconfig to accept distro specific templates paths + + -- Ryan Harper Wed, 05 Sep 2018 16:03:46 -0500 + +cloud-init (18.3-35-g3f6d0972-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - Add unit tests for config/cc_ssh.py [Francis Ginther] + - Fix the built-in cloudinit/tests/helpers:skipIf + - read-version: enhance error message [Joshua Powers] + - hyperv_reporting_handler: simplify threaded publisher + - VMWare: Fix a network config bug in vm with static IPv4 and no gateway. + [Pengpeng Sun] + - logging: Add logging config type hyperv for reporting via Azure KVP + [Andy Liu] + + -- Chad Smith Sat, 01 Sep 2018 12:08:52 -0600 + +cloud-init (18.3-29-gdab59087-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - tests: disable other snap test as well [Joshua Powers] + - tests: disable snap, fix write_files binary [Joshua Powers] + - Add datasource Oracle Compute Infrastructure (OCI). + - azure: allow azure to generate network configuration from IMDS per boot. + - Scaleway: Add network configuration to the DataSource [Louis Bouchard] + + -- Scott Moser Fri, 24 Aug 2018 16:13:36 -0400 + +cloud-init (18.3-24-gf6249277-0ubuntu1) cosmic; urgency=medium + + * debian/cloud-init.templates: enable Oracle Compute Infrastructure by + default + * New upstream snapshot. + - docs: Fix example cloud-init analyze command to match output. + [Wesley Gao] + - netplan: Correctly render macaddress on a bonds and bridges when + provided. + - tools: Add 'net-convert' subcommand command to 'cloud-init devel'. + - redhat: remove ssh keys on new instance. + - Use typeset or local in profile.d scripts. + - OpenNebula: Fix null gateway6 [Akihiko Ota] + + -- Chad Smith Thu, 09 Aug 2018 10:27:29 -0600 + +cloud-init (18.3-18-g3cee0bf8-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - oracle: fix detect_openstack to report True on OracleCloud.com DMI data + - tests: improve LXDInstance trying to workaround or catch bug. + - update_metadata re-config on every boot comments and tests not quite + right [Mike Gerdts] + - tests: Collect build_info from system if available. + - pylint: Fix pylint warnings reported in pylint 2.0.0. + - get_linux_distro: add support for rhel via redhat-release. + - get_linux_distro: add support for centos6 and rawhide flavors of redhat + - tools: add '--debug' to tools/net-convert.py + - tests: bump the version of paramiko to 2.4.1. + + -- Chad Smith Tue, 31 Jul 2018 12:50:28 -0600 + +cloud-init (18.3-9-g2e62cb8a-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - docs: note in rtd about avoiding /tmp when writing files + - ubuntu,centos,debian: get_linux_distro to align with platform.dist + - Fix boothook docs on environment variable name (INSTANCE_I -> + INSTANCE_ID) [Marc Tamsky] + - update_metadata: a datasource can support network re-config every boot + - tests: drop salt-minion integration test + - Retry on failed import of gpg receive keys. + - tools: Fix run-container when neither source or binary package requested. + - docs: Fix a small spelling error. [Oz N Tiram] + - tox: use simplestreams from git repository rather than bzr. + + -- Chad Smith Mon, 09 Jul 2018 14:16:03 -0600 + +cloud-init (18.3-0ubuntu1) cosmic; urgency=medium + + * New upstream release. + - release 18.3 (LP: #1777743) + - docs: represent sudo:false in docs for user_groups config module + - Explicitly prevent `sudo` access for user module + [Jacob Bednarz] (LP: #1771468) + + -- Chad Smith Wed, 20 Jun 2018 11:33:36 -0600 + +cloud-init (18.2-77-g4ce67201-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - lxd: Delete default network and detach device if lxd-init created them. + (LP: #1776958) + - openstack: avoid unneeded metadata probe on non-openstack platforms + (LP: #1776701) + - stages: fix tracebacks if a module stage is undefined or empty + [Robert Schweikert] (LP: #1770462) + - Be more safe on string/bytes when writing multipart user-data to disk. + (LP: #1768600) + - Fix get_proc_env for pids that have non-utf8 content in environment. + (LP: #1775371) + - tests: fix salt_minion integration test on bionic and later + - tests: provide human-readable integration test summary when --verbose + - tests: skip chrony integration tests on lxd running artful or older + - test: add optional --preserve-instance arg to integraiton tests + - netplan: fix mtu if provided by network config for all rendered types + (LP: #1774666) + - tests: remove pip install workarounds for pylxd, take upstream fix. + - subp: support combine_capture argument. + - tests: ordered tox dependencies for pylxd install + + -- Chad Smith Fri, 15 Jun 2018 20:05:07 -0600 + +cloud-init (18.2-64-gbbcc5e82-0ubuntu1) cosmic; urgency=medium + + * debian/rules: update version.version_string to contain packaged version. + (LP: #1770712) + * New upstream snapshot. + - util: add get_linux_distro function to replace platform.dist + [Robert Schweikert] (LP: #1745235) + - pyflakes: fix unused variable references identified by pyflakes 2.0.0. + - - Do not use the systemd_prefix macro, not available in this environment + [Robert Schweikert] + - doc: Add config info to ec2, openstack and cloudstack datasource docs + - Enable SmartOS network metadata to work with netplan via per-subnet + routes [Dan McDonald] (LP: #1763512) + + -- Chad Smith Mon, 04 Jun 2018 12:18:16 -0600 + +cloud-init (18.2-59-gcd1de5f4-0ubuntu1) cosmic; urgency=medium + + * New upstream snapshot. + - openstack: Allow discovery in init-local using dhclient in a sandbox. + (LP: #1749717) + - tests: Avoid using https in httpretty, improve HttPretty test case. + (LP: #1771659) + - yaml_load/schema: Add invalid line and column nums to error message + - Azure: Ignore NTFS mount errors when checking ephemeral drive + [Paul Meyer] + - packages/brpm: Get proper dependencies for cmdline distro. + - packages: Make rpm spec files patch in package version like in debs. + - tools/run-container: replace tools/run-centos with more generic. + - Update version.version_string to contain packaged version. (LP: #1770712) + - cc_mounts: Do not add devices to fstab that are already present. + [Lars Kellogg-Stedman] + - ds-identify: ensure that we have certain tokens in PATH. (LP: #1771382) + - tests: enable Ubuntu Cosmic in integration tests [Joshua Powers] + - read_file_or_url: move to url_helper, fix bug in its FileResponse. + - cloud_tests: help pylint + - flake8: fix flake8 errors in previous commit. + - typos: Fix spelling mistakes in cc_mounts.py log messages [Stephen Ford] + - tests: restructure SSH and initial connections [Joshua Powers] + - ds-identify: recognize container-other as a container, test SmartOS. + - cloud-config.service: run After snap.seeded.service. (LP: #1767131) + + + -- Chad Smith Wed, 23 May 2018 16:16:53 -0600 +cloud-init (18.2-41-g3b712fce-0ubuntu1) cosmic; urgency=medium + + * debian/new-upstream-snapshot: Remove script, now maintained elsewhere. + * New upstream snapshot. + - tests: do not rely on host /proc/cmdline in test_net.py + [Lars Kellogg-Stedman] (LP: #1769952) + - ds-identify: Remove dupe call to is_ds_enabled, improve debug message. + - SmartOS: fix get_interfaces for nics that do not have addr_assign_type. + - tests: fix package and ca_cert cloud_tests on bionic (LP: #1769985) + - ds-identify: make shellcheck 0.4.6 happy with ds-identify. + - pycodestyle: Fix deprecated string literals, move away from flake8. + - azure: Add reported ready marker file. [Joshua Chan] (LP: #1765214) + - tools: Support adding a release suffix through packages/bddeb. + - FreeBSD: Invoke growfs on ufs filesystems such that it does not prompt. + [Harm Weites] (LP: #1404745) + - tools: Re-use the orig tarball in packages/bddeb if it is around. + - netinfo: fix netdev_pformat when a nic does not have an address + assigned. (LP: #1766302) + - collect-logs: add -v flag, write to stderr, limit journal to single + boot. (LP: #1766335) + - IBMCloud: Disable config-drive and nocloud only if IBMCloud is enabled. + (LP: #1766401) + - Add reporting events and log_time around early source of blocking time + + -- Scott Moser Fri, 11 May 2018 16:37:02 -0400 + +cloud-init (18.2-27-g6ef92c98-0ubuntu1~18.04.1) bionic; urgency=medium + + * debian/control: add missing dependency on iproute2. (LP: #1766711) + * debian/control: add missing dependency on isc-dhcp-client. (LP: #1766714) + * New upstream snapshot. (LP: #1767412) + - IBMCloud: recognize provisioning environment during debug boots. + - net: detect unstable network names and trigger a settle if needed + - IBMCloud: improve documentation in datasource. + - sysconfig: dhcp6 subnet type should not imply dhcpv4 [Vitaly Kuznetsov] + - packages/debian/control.in: add missing dependency on iproute2. + - DataSourceSmartOS: add locking of serial device. [Mike Gerdts] + - DataSourceSmartOS: sdc:hostname is ignored [Mike Gerdts] + - DataSourceSmartOS: list() should always return a list [Mike Gerdts] + - schema: in validation, raise ImportError if strict but no jsonschema. + - set_passwords: Add newline to end of sshd config, only restart if + updated. + - pylint: pay attention to unused variable warnings. + - doc: Add documentation for AliYun datasource. [Junjie Wang] + - Schema: do not warn on duplicate items in commands. + + -- Scott Moser Fri, 27 Apr 2018 12:30:21 -0400 + +cloud-init (18.2-14-g6d48d265-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - net: Depend on iproute2's ip instead of net-tools ifconfig or route + - DataSourceSmartOS: fix hang when metadata service is down + [Mike Gerdts] (LP: #1667735) + - DataSourceSmartOS: change default fs on ephemeral disk from ext3 to + ext4. [Mike Gerdts] (LP: #1763511) + - pycodestyle: Fix invalid escape sequences in string literals. + - Implement bash completion script for cloud-init command line + + -- Chad Smith Wed, 18 Apr 2018 15:25:53 -0600 + +cloud-init (18.2-9-g49b562c9-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - tools: Fix make-tarball cli tool usage for development + - renderer: support unicode in render_from_file. + - Implement ntp client spec with auto support for distro selection + (LP: #1749722) + - Apport: add Brightbox, IBM, LXD, and OpenTelekomCloud to list of clouds. + - tests: fix ec2 integration network metadata validation + + -- Chad Smith Thu, 12 Apr 2018 16:06:24 -0600 + +cloud-init (18.2-4-g05926e48-0ubuntu2) bionic; urgency=medium + + * debian/cloud-init.templates: enable IBMCloud by default (LP: #1762773). + + -- Scott Moser Tue, 10 Apr 2018 21:21:17 -0400 + +cloud-init (18.2-4-g05926e48-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - tests: fix integration tests to support lxd 3.0 release + - correct documentation to match correct attribute name usage. + [Dominic Schlegel] (LP: #1420018) + - cc_resizefs, util: handle no /dev/zfs + - doc: Fix links in OpenStack datasource documentation. + [Dominic Schlegel] (LP: #1721660) + + -- Chad Smith Tue, 03 Apr 2018 16:54:45 -0600 + +cloud-init (18.2-0ubuntu1) bionic; urgency=medium + + * debian/control: Add missing dependency on isc-dhcp-client (LP: #1759307). + * New upstream snapshot. + - release 18.2 (LP: #1759318) + - Hetzner: Exit early if dmi system-manufacturer is not Hetzner. + - Add missing dependency on isc-dhcp-client to trunk ubuntu packaging. + (LP: #1759307) + - FreeBSD: resizefs module now able to handle zfs/zpool. + [Dominic Schlegel] (LP: #1721243) + - cc_puppet: Revert regression of puppet creating ssl and ssl_cert dirs + - Enable IBMCloud datasource in settings.py. + + -- Chad Smith Tue, 27 Mar 2018 14:59:58 -0600 + +cloud-init (18.1-35-ge0f644b7-0ubuntu2) bionic; urgency=medium + + * debian/: drop grub-legacy-ec2 package, which is now in its own source + package (LP: #1758420). + + -- Scott Moser Mon, 26 Mar 2018 16:54:34 -0400 + +cloud-init (18.1-35-ge0f644b7-0ubuntu1) bionic; urgency=medium + + * debian/cloud-init.templates: enable IBMCloud datasource. + * New upstream snapshot. + - IBMCloud: Initial IBM Cloud datasource. + - tests: remove jsonschema from xenial tox environment. + - tests: Fix newly added schema unit tests to skip if no jsonschema. + - ec2: Adjust ec2 datasource after exception_cb change. + - Reduce AzurePreprovisioning HTTP timeouts. + [Douglas Jordan] (LP: #1752977) + - Revert the logic of exception_cb in read_url. + [Kurt Garloff] (LP: #1702160, #1298921) + - ubuntu-advantage: Add new config module to support + ubuntu-advantage-tools + - Handle global dns entries in netplan (LP: #1750884) + - Identify OpenTelekomCloud Xen as OpenStack DS. + [Kurt Garloff] (LP: #1756471) + + -- Chad Smith Fri, 23 Mar 2018 17:20:47 -0600 + +cloud-init (18.1-26-g685f9901-0ubuntu1) bionic; urgency=medium + + * debian/cloud-init.templates: Enable Hetzner Cloud datasource. + * New upstream snapshot. + - datasources: fix DataSource subclass get_hostname method signature + (LP: #1757176) + - OpenNebula: Update network to return v2 config rather than ENI. + [Akihiko Ota] + - Add Hetzner Cloud DataSource [Markus Schade] + + -- Chad Smith Tue, 20 Mar 2018 16:39:07 -0600 + +cloud-init (18.1-23-gde34dc7c-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - net: recognize iscsi root cases without ip= on kernel command line. + (LP: #1752391) + - tests: fix flakes warning for unused variable + - tests: patch leaked stderr messages from snap unit tests + - cc_snap: Add new module to install and configure snapd and snap + packages. + - tests: Make pylint happy and fix python2.6 uses of assertRaisesRegex. + - netplan: render bridge port-priority values (LP: #1735821) + + -- Chad Smith Fri, 16 Mar 2018 15:48:04 -0600 + +cloud-init (18.1-17-g97012fbb-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - util: Fix subp regression. Allow specifying subp command as a string. + (LP: #1755965) + - doc: fix all warnings issued by 'tox -e doc' + - FreeBSD: Set hostname to FQDN. [Dominic Schlegel] (LP: #1753499) + - tests: fix run_tree and bddeb + - tests: Fix some warnings in tests that popped up with newer python. + - set_hostname: When present in metadata, set it before network bringup. + (LP: #1746455) + - tests: Centralize and re-use skipTest based on json schema presense. + - This commit fixes get_hostname on the AzureDataSource. + [Douglas Jordan] (LP: #1754495) + - shellify: raise TypeError on bad input. + - Make salt minion module work on FreeBSD. + [Dominic Schlegel] (LP: #1721503) + - Simplify some comparisions. [Rémy Léone] + - Change some list creation and population to literal. [Rémy Léone] + + -- Chad Smith Thu, 15 Mar 2018 14:48:29 -0600 + +cloud-init (18.1-5-g40e77380-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - GCE: fix reading of user-data that is not base64 encoded. (LP: #1752711) + - doc: fix chef install from apt packages example in RTD. + - Implement puppet 4 support [Romanos Skiadas] (LP: #1446804) + - subp: Fix subp usage with non-ascii characters when no system locale. + (LP: #1751051) + - salt: configure grains in grains file rather than in minion config. + [Daniel Wallace] + + -- Chad Smith Thu, 01 Mar 2018 15:47:04 -0700 + +cloud-init (18.1-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - release 18.1 (LP: #1751145) + - OVF: Fix VMware support for 64-bit platforms. [Sankar Tanguturi] + - ds-identify: Fix searching for iso9660 OVF cdroms. (LP: #1749980) + - SUSE: Fix groups used for ownership of cloud-init.log [Robert Schweikert] + - ds-identify: check /writable/system-data/ for nocloud seed. + (LP: #1747070) + - tests: run nosetests in cloudinit/ directory, fix py26 fallout. + - tools: run-centos: git clone rather than tar. + - tests: add support for logs with lxd from snap and future lxd 3. + (LP: #1745663) + + -- Chad Smith Thu, 22 Feb 2018 15:42:11 -0700 + +cloud-init (17.2-34-g644048e3-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - EC2: Fix get_instance_id called against cached datasource pickle. + [Chad Smith] (LP: #1748354) + - cli: fix cloud-init status to report running when before result.json + [Chad Smith] (LP: #1747965) + - net: accept network-config in netplan format for renaming interfaces + [Ryan Harper] (LP: #1709715) + - Fix ssh keys validation in ssh_util [Tatiana Kholkina] + + -- Scott Moser Fri, 09 Feb 2018 17:04:03 -0500 + +cloud-init (17.2-30-gf7deaf15-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - docs: Update RTD content for cloud-init subcommands. + - OVF: Extend well-known labels to include OVFENV. (LP: #1698669) + - Fix potential cases of uninitialized variables. (LP: #1744796) + - tests: Collect script output as binary, collect systemd journal, fix lxd. + - HACKING.rst: mention setting user name and email via git config. + + -- Chad Smith Fri, 02 Feb 2018 09:51:14 -0700 + +cloud-init (17.2-25-gc03bdd3d-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - Azure VM Preprovisioning support. [Douglas Jordan] (LP: #1734991) + - tools/read-version: Fix read-version when in a git worktree. + - docs: Fix typos in docs and one debug message. [Florian Grignon] + - btrfs: support resizing if root is mounted ro. + [Robert Schweikert] (LP: #1734787) + - OpenNebula: Improve network configuration support. + [Akihiko Ota] (LP: #1719157, #1716397, #1736750) + + -- Chad Smith Wed, 24 Jan 2018 16:39:23 -0700 + +cloud-init (17.2-20-g32a6a176-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - tests: Fix EC2 Platform to return console output as bytes. + - tests: Fix attempted use of /run in a test case. + - GCE: Improvements and changes to ssh key behavior for default user. + [Max Illfelder] (LP: #1670456, #1707033, #1707037, #1707039) + - subp: make ProcessExecutionError have expected types in stderr, stdout. + - tests: when querying ntp server, do not do dns resolution. + - Recognize uppercase vfat disk labels [James Penick] (LP: #1598783) + - tests: remove zesty as supported OS to test + + -- Chad Smith Tue, 23 Jan 2018 20:10:44 -0700 + +cloud-init (17.2-13-g6299e8d0-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - Do not log warning on config files that represent None. (LP: #1742479) + - tests: Use git hash pip dependency format for pylxd. [Chad Smith] + - tests: add integration requirements text file [Joshua Powers] + - MAAS: add check_instance_id based off oauth tokens. (LP: #1712680) + + -- Scott Moser Mon, 15 Jan 2018 10:15:05 -0500 + +cloud-init (17.2-9-gdf24daa8-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - tests: update apt sources list test [Joshua Powers] + - tests: clean up image properties [Joshua Powers] + - tests: rename test ssh keys to avoid appearance of leaking private keys. + [Joshua Powers] + - tests: Enable AWS EC2 Integration Testing [Joshua Powers] + - cli: cloud-init clean handles symlinks [Chad Smith] (LP: #1741093) + - SUSE: Add a basic test of network config rendering. [Robert Schweikert] + - Azure: Only bounce network when necessary. [Chad Smith] (LP: #1722668) + - lint: Fix lints seen by pylint version 1.8.1. [Chad Smith] + + -- Scott Moser Mon, 15 Jan 2018 06:42:30 -0500 + +cloud-init (17.2-1-g4089e20c-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - cli: Fix error in cloud-init modules --mode=init. + (LP: #1736600) + - release 17.2 + + -- Chad Smith Fri, 15 Dec 2017 10:11:50 -0700 + +cloud-init (17.1-60-ga30a3bb5-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - ds-identify: failure in NoCloud due to unset variable usage. + (LP: #1737704) + - tests: fix collect_console when not implemented [Joshua Powers] + + -- Chad Smith Tue, 12 Dec 2017 12:03:08 -0700 + +cloud-init (17.1-58-g703241a3-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - ec2: Use instance-identity doc for region and instance-id + [Andrew Jorgensen] + - tests: remove leaked tmp files in config drive tests. + - setup.py: Do not include rendered files in SOURCES.txt + - SUSE: remove delta in systemd local template for SUSE [Robert Schweikert] + - tests: move to using tox 1.7.5 + + -- Scott Moser Mon, 11 Dec 2017 18:30:56 -0500 + +cloud-init (17.1-53-ga5dc0f42-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - OVF: improve ds-identify to support finding OVF iso transport. + (LP: #1731868) + - VMware: Support for user provided pre and post-customization scripts + [Maitreyee Saikia] + + -- Chad Smith Fri, 08 Dec 2017 14:46:36 -0700 + +cloud-init (17.1-51-g05b2308a-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - citest: In NoCloudKVM provide keys via metadata not userdata. + - pylint: Update pylint to 1.7.1, run on tests/ and tools and fix + complaints. [Ryan Harper] + - Datasources: Formalize DataSource get_data and related properties. + [Chad Smith] + - cli: Add clean and status subcommands [Chad Smith] + - tests: consolidate platforms into specific dirs + + -- Scott Moser Thu, 07 Dec 2017 22:58:41 -0500 + +cloud-init (17.1-46-g7acc9e68-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - ec2: Fix sandboxed dhclient background process cleanup. + [Chad Smith] (LP: #1735331) + - tests: NoCloudKVMImage do not modify the original local cache image. + - tests: Enable bionic in integration tests. [Joshua Powers] + - tests: Use apt-get to install a deb so that depends get resolved. + - sysconfig: Correctly render dns and dns search info. + [Ryan McCabe] (LP: #1705804) + + -- Scott Moser Thu, 30 Nov 2017 21:59:55 -0500 + +cloud-init (17.1-41-g76243487-0ubuntu1) bionic; urgency=medium + + * debian/cloud-init.templates: Fix capitilazation in 'AliYun' name. + (LP: #1728186) + * New upstream snapshot. + - integration test: replace curtin test ppa with cloud-init test ppa. + - EC2: Fix bug using fallback_nic and metadata when restoring from cache. + (LP: #1732917) + - EC2: Kill dhclient process used in sandbox dhclient. (LP: #1732964) + - ntp: fix configuration template rendering for openSUSE and SLES + (LP: #1726572) + - centos: Provide the failed #include url in error messages + - Catch UrlError when #include'ing URLs [Andrew Jorgensen] + - hosts: Fix openSUSE and SLES setup for /etc/hosts and clarify docs. + [Robert Schweikert] (LP: #1731022) + - rh_subscription: Perform null checks for enabled and disabled repos. + [Dave Mulford] + - Improve warning message when a template is not found. + [Robert Schweikert] (LP: #1731035) + - Replace the temporary i9n.brickies.net with i9n.cloud-init.io. + - Azure: don't generate network configuration for SRIOV devices + (LP: #1721579) + - tests: address some minor feedback missed in last merge. + - tests: integration test cleanup and full pass of nocloud-kvm. + - Gentoo: chmod +x on all files in sysvinit/gentoo/ + [ckonstanski] (LP: #1727126) + + -- Chad Smith Mon, 20 Nov 2017 15:18:52 -0700 + +cloud-init (17.1-27-geb292c18-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - EC2: Limit network config to fallback nic, fix local-ipv4 only + instances. (LP: #1728152) + - Gentoo: Use "rc-service" rather than "service". + [ckonstanski] (LP: #1727121) + + -- Chad Smith Tue, 31 Oct 2017 12:51:10 -0600 + +cloud-init (17.1-25-g17a15f9e-0ubuntu1) bionic; urgency=medium + + * New upstream snapshot. + - resizefs: Fix regression when system booted with root=PARTUUID= + [Chad Smith] (LP: #1725067) + - tools: make yum package installation more reliable + - citest: fix remaining warnings raised by integration tests. + - citest: show the class actual class name in results. + - ntp: fix config module schema to allow empty ntp config + [Chad Smith] (LP: #1724951) + - tools: disable fastestmirror if using proxy [Joshua Powers] + - schema: Log debug instead of warning when jsonschema is not available. + (LP: #1724354) + + -- Ryan Harper Tue, 24 Oct 2017 10:40:00 -0500 + +cloud-init (17.1-18-gd4f70470-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - simpletable: Fix get_string method to return table-formatted string + (LP: #1722566) + + -- Chad Smith Tue, 10 Oct 2017 13:40:13 -0600 + +cloud-init (17.1-17-g45d361cb-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - net: Handle bridge stp values of 0 and convert to boolean type + [Chad Smith] + - tools: Give specific --abbrev=8 to "git describe" + - network: bridge_stp value not always correct [Ryan Harper] (LP: #1721157) + - tests: re-enable tox with nocloud-kvm support [Joshua Powers] + + -- Ryan Harper Thu, 05 Oct 2017 16:15:34 -0500 + +cloud-init (17.1-13-g7fd04255-0ubuntu1) artful; urgency=medium + + * debian/copyright: dep5 updates, reorganize, add Apache 2.0 license. + (LP: #1718681) + * debian/control: drop dependency on python3-prettytable + * New upstream snapshot. + - systemd: remove limit on tasks created by cloud-init-final.service. + [Robert Schweikert] (LP: #1717969) + - suse: Support addition of zypper repos via cloud-config. + [Robert Schweikert] (LP: #1718675) + - tests: Combine integration configs and testcases [Joshua Powers] + - Azure, CloudStack: Support reading dhcp options from systemd-networkd. + [Dimitri John Ledkov] (LP: #1718029) + - packages/debian/copyright: remove mention of boto and MIT license + - systemd: only mention Before=apt-daily.service on debian based distros. + [Robert Schweikert] + - Add missing simpletable and simpletable tests for failed merge + [Chad Smith] + - Remove prettytable dependency, introduce simpletable [Andrew Jorgensen] + - debian/copyright: dep5 updates, reorganize, add Apache 2.0 license. + [Joshua Powers] (LP: #1718681) + - tests: remove dependency on shlex [Joshua Powers] + - AltCloud: Trust PATH for udevadm and modprobe. + - DataSourceOVF: use util.find_devs_with(TYPE=iso9660) + [Ryan Harper] (LP: #1718287) + - tests: remove a temp file used in bootcmd tests. + + -- Ryan Harper Tue, 03 Oct 2017 10:59:52 -0500 + +cloud-init (17.1-0ubuntu1) artful; urgency=medium + + * New upstream release. + - doc: document GCE datasource. [Arnd Hannemann] + - suse: updates to templates to support openSUSE and SLES. + [Robert Schweikert] (LP: #1718640) + - suse: Copy sysvinit files from redhat with slight changes. + [Robert Schweikert] (LP: #1718649) + - docs: fix sphinx module schema documentation [Chad Smith] + - tests: Add cloudinit package to all test targets [Chad Smith] + - Makefile: No longer look for yaml files in obsolete ./bin/. + - tests: fix ds-identify unit tests to set EC2_STRICT_ID_DEFAULT. + + -- Scott Moser Fri, 22 Sep 2017 12:21:18 -0400 + +cloud-init (0.7.9-283-g7eb3460b-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - ec2: Fix maybe_perform_dhcp_discovery to use /var/tmp as a tmpdir + [Chad Smith] (LP: #1717627) + - Azure: wait longer for SSH pub keys to arrive. + [Paul Meyer] (LP: #1717611) + + -- Scott Moser Mon, 18 Sep 2017 20:41:53 -0400 + +cloud-init (0.7.9-281-g10f067d8-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - GCE: Fix usage of user-data. (LP: #1717598) + + -- Scott Moser Mon, 18 Sep 2017 17:03:22 -0400 + +cloud-init (0.7.9-280-ge626966e-0ubuntu1) artful; urgency=medium + + * debian/rules: install rsyslog file with 0644 mode instead of 0755. + * debian/rules, debian/apport-launcher.py: add an apport hook. (LP: #1607345) + * New upstream snapshot. + - cmdline: add collect-logs subcommand. [Chad Smith] (LP: #1607345) + - CloudStack: consider dhclient lease files named with a hyphen. + (LP: #1717147) + - resizefs: Drop check for read-only device file, do not warn on + overlayroot. [Chad Smith] + - Do not provide systemd-fsck drop-in which could cause ordering cycles. + [Balint Reczey] (LP: #1717477) + - tests: Enable the NoCloud KVM platform [Joshua Powers] + - resizefs: pass mount point to xfs_growfs [Dusty Mabe] + - vmware: Enable nics before sending the SUCCESS event. [Sankar Tanguturi] + - cloud-config modules: honor distros definitions in each module + [Chad Smith] (LP: #1715738, #1715690) + - chef: Add option to pin chef omnibus install version + [Ethan Apodaca] (LP: #1462693) + - tests: execute: support command as string [Joshua Powers] + - schema and docs: Add jsonschema to resizefs and bootcmd modules + [Chad Smith] + - tools: Add xkvm script, wrapper around qemu-system [Joshua Powers] + - vmware customization: return network config format + [Sankar Tanguturi] (LP: #1675063) + + -- Scott Moser Fri, 15 Sep 2017 16:09:07 -0400 + +cloud-init (0.7.9-267-g922c3c5c-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - Ec2: only attempt to operate at local mode on known platforms. + (LP: #1715128) + - Use /run/cloud-init for tempfile operations. (LP: #1707222) + - ds-identify: Make OpenStack return maybe on arch other than intel. + (LP: #1715241) + - tests: mock missed openstack metadata uri network_data.json + [Chad Smith] (LP: #1714376) + - relocate tests/unittests/helpers.py to cloudinit/tests + [Lars Kellogg-Stedman] + - tox: add nose timer output [Joshua Powers] + - upstart: do not package upstart jobs, drop ubuntu-init-switch module. + - tests: Stop leaking calls through unmocked metadata addresses + [Chad Smith] (LP: #1714117) + + -- Scott Moser Thu, 07 Sep 2017 16:59:04 -0400 + +cloud-init (0.7.9-259-g7e76c57b-0ubuntu1) artful; urgency=medium + + * Disable upstart support, as upstart is no longer shipped in artful. + [Dimitri Ledkov] + * New upstream snapshot. + - distro: allow distro to specify a default locale [Ryan Harper] + - tests: fix two recently added tests for sles distro. + - url_helper: dynamically import oauthlib import from inside oauth_headers + [Chad Smith] + - tox: make xenial environment run with python3.6 + - suse: Add support for openSUSE and return SLES to a working state. + [Robert Schweikert] + - GCE: Add a main to the GCE Datasource. + - ec2: Add IPv6 dhcp support to Ec2DataSource. [Chad Smith] (LP: #1639030) + - url_helper: fail gracefully if oauthlib is not available + [Lars Kellogg-Stedman] (LP: #1713760) + - cloud-init analyze: fix issues running under python 2. [Andrew Jorgensen] + - Configure logging module to always use UTC time. + [Ryan Harper] (LP: #1713158) + - Log a helpful message if a user script does not include shebang. + [Andrew Jorgensen] + - cli: Fix command line parsing of coniditionally loaded subcommands. + [Chad Smith] (LP: #1712676) + - doc: Explain error behavior in user data include file format. + [Jason Butz] + - cc_landscape & cc_puppet: Fix six.StringIO use in writing configs + [Chad Smith] (LP: #1699282, #1710932) + - schema cli: Add schema subcommand to cloud-init cli and cc_runcmd schema + [Chad Smith] + - Debian: Remove non-free repositories from apt sources template. + [Joonas Kylmälä] (LP: #1700091) + + -- Scott Moser Wed, 30 Aug 2017 21:18:05 -0400 + +cloud-init (0.7.9-243-ge74d775-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - tools: Add tooling for basic cloud-init performance analysis. + [Chad Smith] (LP: #1709761) + - network: add v2 passthrough and fix parsing v2 config with bonds/bridge + params [Ryan Harper] (LP: #1709180) + - doc: update capabilities with features available, link doc reference, + cli example [Ryan Harper] + - vcloud directory: Guest Customization support for passwords + [Maitreyee Saikia] + - ec2: Allow Ec2 to run in init-local using dhclient in a sandbox. + [Chad Smith] (LP: #1709772) + - cc_ntp: fallback on timesyncd configuration if ntp is not installable + [Ryan Harper] (LP: #1686485) + - net: Reduce duplicate code. Have get_interfaces_by_mac use + get_interfaces. + - tests: Fix build tree integration tests [Joshua Powers] + - sysconfig: Dont repeat header when rendering resolv.conf + [Ryan Harper] (LP: #1701420) + - archlinux: Fix bug with empty dns, do not render 'lo' devices. + (LP: #1663045, #1706593) + - cloudinit.net: add initialize_network_device function and tests + [Chad Smith] + - makefile: fix ci-deps-ubuntu target [Chad Smith] + + -- Ryan Harper Mon, 21 Aug 2017 15:09:36 -0500 + +cloud-init (0.7.9-231-g80bf98b9-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - tests: remove 'yakkety' from releases as it is EOL. + - systemd: make systemd-fsck run after cloud-init.service (LP: #1691489) + - tests: Add initial tests for EC2 and improve a docstring. + - locale: Do not re-run locale-gen if provided locale is system default. + - archlinux: fix set hostname usage of write_file. + [Joshua Powers] (LP: #1705306) + - sysconfig: support subnet type of 'manual'. + - Drop rand_str() usage in DNS redirection detection + [Bob Aman] (LP: #1088611) + + -- Scott Moser Mon, 31 Jul 2017 09:47:34 -0400 + +cloud-init (0.7.9-221-g7e41b2a7-0ubuntu3) artful; urgency=medium + + * debian/control: replace Depends on ifupdown with 'nplan | ifupdown'. + (LP: #1705639) + * debian/control: drop versioned dependencies on old packages. + + -- Scott Moser Fri, 21 Jul 2017 12:32:33 -0400 + +cloud-init (0.7.9-221-g7e41b2a7-0ubuntu2) artful; urgency=medium + + * debian/cloud-init.templates: enable Scaleway cloud. + * debian/cloud-init.templates: enable Aliyun cloud. + + -- Scott Moser Fri, 21 Jul 2017 09:33:39 -0400 + +cloud-init (0.7.9-221-g7e41b2a7-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - sysconfig: use MACADDR on bonds/bridges to configure mac_address + [Ryan Harper] (LP: #1701417) + - net: eni route rendering missed ipv6 default route config + [Ryan Harper] (LP: #1701097) + - sysconfig: enable mtu set per subnet, including ipv6 mtu + [Ryan Harper] (LP: #1702513) + - sysconfig: handle manual type subnets [Ryan Harper] (LP: #1687725) + - sysconfig: fix ipv6 gateway routes [Ryan Harper] (LP: #1694801) + - sysconfig: fix rendering of bond, bridge and vlan types. + [Ryan Harper] (LP: #1695092) + - Templatize systemd unit files for cross distro deltas. [Ryan Harper] + - sysconfig: ipv6 and default gateway fixes. [Ryan Harper] (LP: #1704872) + - net: fix renaming of nics to support mac addresses written in upper case. + (LP: #1705147) + + -- Scott Moser Thu, 20 Jul 2017 21:37:12 -0400 + +cloud-init (0.7.9-212-g865e941f-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - tests: fixes for issues uncovered when moving to python 3.6. + - sysconfig: include GATEWAY value if set in subnet + [Ryan Harper] (LP: #1686856) + + -- Scott Moser Wed, 19 Jul 2017 07:54:57 -0400 + +cloud-init (0.7.9-210-ge80517ae-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - Scaleway: add datasource with user and vendor data for Scaleway. + [Julien Castets] + - Support comments in content read by load_shell_content. + - FreeBSD: cloudinit local fail to run during boot [Hongjiang Zhang] + - doc: fix disk setup example table_type options [Sandor Zeestraten] + (LP: #1703789) + - tools: Fix exception handling. [Joonas Kylmälä] (LP: #1701527) + - tests: fix usage of mock in GCE test. + - test_gce: Fix invalid mock of platform_reports_gce to return False + [Chad Smith] + - test: fix incorrect keyid for apt repository. [Joshua Powers] + (LP: #1702717) + - tests: Update version of pylxd [Joshua Powers] + - write_files: Remove log from helper function signatures. + [Andrew Jorgensen] + - doc: document the cmdline options to NoCloud [Brian Candler] + + -- Scott Moser Tue, 18 Jul 2017 14:03:33 -0400 + +cloud-init (0.7.9-199-g4d9f24f5-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - read_dmi_data: always return None when inside a container. (LP: #1701325) + - requirements.txt: remove trailing white space. + + -- Scott Moser Thu, 29 Jun 2017 18:01:21 -0400 + +cloud-init (0.7.9-197-gebc9ecbc-0ubuntu1) artful; urgency=medium + + * debian/control: add build dependency python3-jsonschema (LP: #1695318) + * New upstream snapshot. + - Azure: Add network-config, Refactor net layer to handle duplicate macs. + [Ryan Harper] + - Tests: Simplify the check on ssh-import-id [Joshua Powers] + - tests: update ntp tests after sntp added [Joshua Powers] + - FreeBSD: Make freebsd a variant, fix unittests and + tools/build-on-freebsd. + - FreeBSD: fix test failure + - FreeBSD: replace ifdown/ifup with "ifconfig down" and "ifconfig up". + [Hongjiang Zhang] (LP: #1697815) + - FreeBSD: fix cdrom mounting failure if /mnt/cdrom/secure did not exist. + [Hongjiang Zhang] (LP: #1696295) + - main: Don't use templater to format the welcome message + [Andrew Jorgensen] + - docs: Automatically generate module docs form schema if present. + [Chad Smith] + - debian: fix path comment in /etc/hosts template. + [Jens Sandmann] (LP: #1606406) + - suse: add hostname and fully qualified domain to template. + [Jens Sandmann] + - write_file(s): Print permissions as octal, not decimal [Andrew Jorgensen] + - ci deps: Add --test-distro to read-dependencies to install all deps + [Chad Smith] + - tools/run-centos: cleanups and move to using read-dependencies + - pkg build ci: Add make ci-deps- target to install pkgs + [Chad Smith] + - systemd: make cloud-final.service run before apt daily services. + (LP: #1693361) + - selinux: Allow restorecon to be non-fatal. [Ryan Harper] (LP: #1686751) + - net: Allow netinfo subprocesses to return 0 or 1. + [Ryan Harper] (LP: #1686751) + - net: Allow for NetworkManager configuration [Ryan McCabe] (LP: #1693251) + - Use distro release version to determine if we use systemd in redhat spec + [Ryan Harper] + - net: normalize data in network_state object + - Integration Testing: tox env, pyxld 2.2.3, and revamp framework + [Wesley Wiedenmeier] + - Chef: Update omnibus url to chef.io, minor doc changes. [JJ Asghar] + - tools: add centos scripts to build and test [Joshua Powers] + - Drop cheetah python module as it is not needed by trunk [Ryan Harper] + - rhel/centos spec cleanups. + - cloud.cfg: move to a template. setup.py changes along the way. + - Makefile: add deb-src and srpm targets. use PYVER more places. + - makefile: fix python 2/3 detection in the Makefile [Chad Smith] + - snap: Removing snapcraft plug line [Joshua Powers] (LP: #1695333) + - RHEL/CentOS: Fix default routes for IPv4/IPv6 configuration. + [Andreas Karis] (LP: #1696176) + - test: Fix pyflakes complaint of unused import. + [Joshua Powers] (LP: #1695918) + - NoCloud: support seed of nocloud from smbios information + [Vladimir Pouzanov] (LP: #1691772) + - net: when selecting a network device, use natural sort order + [Marc-Aurèle Brothier] + - fix typos and remove whitespace in various docs [Stephan Telling] + - systemd: Fix typo in comment in cloud-init.target. [Chen-Han Hsiao] + - Tests: Skip jsonschema related unit tests when dependency is absent. + [Chad Smith] (LP: #1695318) + - azure: remove accidental duplicate line in merge. + - azure: identify platform by well known value in chassis asset tag. + [Chad Smith] (LP: #1693939) + - tools/net-convert.py: support old cloudinit versions by using kwargs. + - ntp: Add schema definition and passive schema validation. + [Chad Smith] (LP: #1692916) + - Fix eni rendering for bridge params that require repeated key for values. + [Ryan Harper] + - net: remove systemd link file writing from eni renderer [Ryan Harper] + - AliYun: Enable platform identification and enable by default. + [Junjie Wang] (LP: #1638931) + + -- Scott Moser Tue, 27 Jun 2017 17:18:24 -0400 + +cloud-init (0.7.9-153-g16a7302f-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - net: fix reading and rendering addresses in cidr format. + [Dimitri John Ledkov] (LP: #1689346, #1684349) + - disk_setup: udev settle before attempting partitioning or fs creation. + [Scott Moser] (LP: #1692093) + - GCE: Update the attribute used to find instance SSH keys. + [Daniel Watkins] (LP: #1693582) + - nplan: For bonds, allow dashed or underscore names of keys. + [Dimitri John Ledkov] (LP: #1690480) + - python2.6: fix unit tests usage of assertNone and format. + - test: update docstring on test_configured_list_with_none + - fix tools/ds-identify to not write None twice. + - tox/build: do not package depend on style requirements. + - cc_ntp: Restructure cc_ntp unit tests. [Chad Smith] (LP: #1692794) + + -- Scott Moser Fri, 26 May 2017 15:11:48 -0400 + +cloud-init (0.7.9-144-g2825a917-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - flake8: move the pinned version of flake8 up to 3.3.0 + - tests: Apply workaround for snapd bug in test case. [Joshua Powers] + - RHEL/CentOS: Fix dual stack IPv4/IPv6 configuration. [Andreas Karis] + (LP: #1679817, #1685534, #1685532) + - disk_setup: fix several issues with gpt disk partitions. (LP: #1692087) + - function spelling & docstring update [Joshua Powers] + - Fixing wrong file name regression. [Joshua Powers] + - tox: move pylint target to 1.7.1 + - Fix get_interfaces_by_mac for empty macs (LP: #1692028) + - DigitalOcean: remove routes except for the public interface. + [Ben Howard] (LP: #1681531.) + - netplan: pass macaddress, when specified, for vlans + [Dimitri John Ledkov] (LP: #1690388) + - doc: various improvements for the docs on cc_users_groups. + [Felix Dreissig] + - cc_ntp: write template before installing and add service restart + [Ryan Harper] (LP: #1645644) + - cloudstack: fix tests to avoid accessing /var/lib/NetworkManager + [Lars Kellogg-Stedman] + - tests: fix hardcoded path to mkfs.ext4 [Joshua Powers] (LP: #1691517) + - Actually skip warnings when .skip file is present. [Chris Brinker] + (LP: #1691551) + - netplan: fix netplan render_network_state signature. + [Dimitri John Ledkov] (LP: #1685944) + - Azure: fix reformatting of ephemeral disks on resize to large types. + (LP: #1686514) + - Revert "tools/net-convert: fix argument order for render_network_state" + - make deb: Add devscripts dependency for make deb. Cleanup packages/bddeb. + [Chad Smith] (LP: #1685935) + - tools/net-convert: fix argument order for render_network_state + [Ryan Harper] (LP: #1685944) + - openstack: fix log message copy/paste typo in _get_url_settings + [Lars Kellogg-Stedman] + - unittests: fix unittests run on centos [Joshua Powers] + - Improve detection of snappy to include os-release and kernel cmdline. + (LP: #1689944) + - Add address to config entry generated by _klibc_to_config_entry. + [Julien Castets] (LP: #1691135) + - sysconfig: Raise ValueError when multiple default gateways are present. + [Chad Smith] (LP: #1687485) + - FreeBSD: improvements and fixes for use on Azure + [Hongjiang Zhang] (LP: #1636345) + - Add unit tests for ds-identify, fix Ec2 bug found. + - fs_setup: if cmd is specified, use shell interpretation. + [Paul Meyer] (LP: #1687712) + - doc: document network configuration defaults policy and formats. + [Ryan Harper] + - Fix name of "uri" key in docs for "cc_apt_configure" module + [Felix Dreissig] + - tests: Enable artful [Joshua Powers] + + -- Scott Moser Tue, 23 May 2017 17:04:40 -0400 + +cloud-init (0.7.9-113-g513e99e0-0ubuntu1) artful; urgency=medium + + * New upstream snapshot. + - nova-lxd: read product_name from environment, not platform. + (LP: #1685810) + - Fix yum repo config where keys contain array values + [Dylan Perry] (LP: #1592150) + - template: Update debian backports template [Joshua Powers] (LP: #1627293) + - rsyslog: replace ~ with stop [Joshua Powers] (LP: #1367899) + - Doc: add additional RTD examples [Joshua Powers] (LP: #1459604) + - Fix growpart for some cases when booted with root=PARTUUID. + (LP: #1684869) + - pylint: update output style to parseable [Joshua Powers] + - pylint: fix all logging warnings [Joshua Powers] + - CloudStack: Add NetworkManager to list of supported DHCP lease dirs. + [Syed Mushtaq Ahmed] + - net: kernel lies about vlans not stealing mac addresses, when they do + [Dimitri John Ledkov] (LP: #1682871) + - ds-identify: Check correct path for "latest" config drive + [Daniel Watkins] (LP: #1673637) + - doc: Fix example for resolve.conf configuration. + [Jon Grimm] (LP: #1531582) + - Fix examples that reference upstream chef repository. + [Jon Grimm] (LP: #1678145) + - doc: correct grammar and improve clarity in merging documentation. + [David Tagatac] + - doc: Add missing doc link to snap-config module. [Ryan Harper] + - snap: allows for creating cloud-init snap [Joshua Powers] + - DigitalOcean: assign IPv4ll address to lowest indexed interface. + [Ben Howard] + - DigitalOcean: configure all NICs presented in meta-data. [Ben Howard] + - Remove (and/or fix) URL shortener references [Jon Grimm] (LP: #1669727) + - HACKING.rst: more info on filling out contributors agreement. + [Scott Moser] + - util: teach write_file about copy_mode option + [Lars Kellogg-Stedman] (LP: #1644064) + - DigitalOcean: bind resolvers to loopback interface. [Ben Howard] + - tests: fix AltCloud tests to not rely on blkid (LP: #1636531) + + -- Scott Moser Tue, 25 Apr 2017 16:34:08 -0400 + +cloud-init (0.7.9-90-g61eb03fe-0ubuntu2) UNRELEASED; urgency=medium + + * debian/update-grub-legacy-ec2: fix early exit failure no /etc/fstab + file. (LP: #1682160) + + -- Scott Moser Wed, 12 Apr 2017 11:16:33 -0400 + +cloud-init (0.7.9-90-g61eb03fe-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - OpenStack: add 'dvs' to the list of physical link types. + (LP: #1674946) + + -- Scott Moser Mon, 03 Apr 2017 11:10:38 -0400 + +cloud-init (0.7.9-89-gbf7723e8-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - Fix bug that resulted in an attempt to rename bonds or vlans. + (LP: #1669860) + - tests: update OpenNebula and Digital Ocean to not rely on host + interfaces. + + -- Scott Moser Fri, 31 Mar 2017 17:02:28 -0400 + +cloud-init (0.7.9-87-gd23543eb-0ubuntu1) zesty; urgency=medium + + * debian/cloud-init.templates: add Bigstep to list of sources. (LP: #1676460) + * New upstream snapshot. + - net: in netplan renderer delete known image-builtin content. + (LP: #1675576) + - doc: correct grammar in capabilities.rst [David Tagatac] + - ds-identify: fix detecting of maas datasource. (LP: #1677710) + - netplan: remove debugging prints, add debug logging [Ryan Harper] + - ds-identify: do not write None twice to datasource_list. + - support resizing partition and rootfs on system booted without initramfs. + (LP: #1677376) [Steve Langasek] + - apt_configure: run only when needed. (LP: #1675185) + - OpenStack: identify OpenStack by product 'OpenStack Compute'. + (LP: #1675349) + - GCE: Search GCE in ds-identify, consider serial number in check. + (LP: #1674861) + - Add support for setting hashed passwords [Tore S. Lonoy] (LP: #1570325) + + -- Scott Moser Thu, 30 Mar 2017 16:46:43 -0400 + +cloud-init (0.7.9-77-g4a2b2f87-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - Fix filesystem creation when using "partition: auto" [Jonathan Ballet] + (LP: #1634678) + - ConfigDrive: support reading config drive data from /config-drive for + nova-lxd. (LP: #1673411) + - ds-identify: fix detection of Bigstep datasource. (LP: #1674766) + - test: add running of pylint [Joshua Powers] + - ds-identify: fix bug where filename expansion was left on. + - advertise network config v2 support (NETWORK_CONFIG_V2) in features. + - Bigstep: fix bug when executing in python3. + - Fix unit test when running in a system deployed with cloud-init. + - Bounce network interface for Azure when using the built-in path. + [Brent Baude] (LP: #1674685) + + -- Scott Moser Fri, 24 Mar 2017 16:50:56 -0400 + +cloud-init (0.7.9-68-gef18b8ac-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - cloudinit.net: add network config v2 parsing and rendering [Ryan Harper] + - net: Fix incorrect call to isfile [Joshua Powers] (LP: #1674317) + + -- Scott Moser Mon, 20 Mar 2017 16:21:55 -0400 + +cloud-init (0.7.9-66-g5beecdf8-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - net: add renderers for automatically selecting the renderer. + - doc: fix config drive doc with regard to unpartitioned disks. + (LP: #1673818) + - test: Adding integratiron test for password as list [Joshua Powers] + - render_network_state: switch arguments around, do not require target + - support 'loopback' as a device type. + - Integration Testing: improve testcase subclassing [Wesley Wiedenmeier] + - gitignore: adding doc/rtd_html [Joshua Powers] + - doc: add instructions for running integration tests via tox. + [Joshua Powers] + - test: avoid differences in 'date' output due to daylight savings. + - Fix chef config module in omnibus install. [Jeremy Melvin] (LP: #1583837) + - Add feature flags to cloudinit.version. [Wesley Wiedenmeier] + - tox: add a citest environment + - Support chpasswd/list being a list in addition to a string. + [Sergio Lystopad] (LP: #1665694) + - doc: Fix configuration example for cc_set_passwords module. + [Sergio Lystopad] (LP: #1665773) + - net: support both ipv4 and ipv6 gateways in sysconfig. + [Lars Kellogg-Stedman] (LP: #1669504) + - net: do not raise exception for > 3 nameservers + [Lars Kellogg-Stedman] (LP: #1670052) + + -- Scott Moser Fri, 17 Mar 2017 21:41:40 -0400 + +cloud-init (0.7.9-48-g1c795b9-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - ds-identify: report cleanups for config and exit value. (LP: #1669949) + + -- Scott Moser Mon, 06 Mar 2017 14:56:28 -0500 + +cloud-init (0.7.9-47-gc81ea53-0ubuntu1) zesty; urgency=medium + + * debian/rules: install Z99-cloudinit-warnings.sh to /etc/profile.d + * New upstream snapshot. + - Support warning if the used datasource is not in ds-identify's list + (LP: #1669675) + - Z99-cloudinit-warnings: Add profile.d script for showing warnings on + login. + - Z99-cloud-locale-test.sh: convert tabs to spaces, remove unneccesary + execute bit in permissions. + - tools/ds-identify: look at cloud.cfg when looking for ec2 strict_id. + - tools/ds-identify: fix bug that would mistakingly enable vmware guest + customization. + + -- Scott Moser Fri, 03 Mar 2017 01:56:19 -0500 + +cloud-init (0.7.9-38-g83606ae-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - tools/ds-identify: ovf identify vmware guest customization. + - Identify Brightbox as an Ec2 datasource user. (LP: #1661693) + - DatasourceEc2: add warning message when not on AWS. + - ds-identify: add reading of datasource/Ec2/strict_id + - tools/ds-identify: add support for 'found' or 'maybe' contributing + config. + - tools/ds-identify: read the seed directory on Ec2 + - tools/ds-identify: use quotes in local declarations. + - tools/ds-identify: fix documentation of policy setting in a comment. + + -- Scott Moser Fri, 24 Feb 2017 22:53:53 -0500 + +cloud-init (0.7.9-30-gf4e8eb0-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - ds-identify: only run once per boot unless --force is given. + - net: correct errors in cloudinit/net/sysconfig.py + [Lars Kellogg-Stedman] (LP: #1665441) + - ec2_utils: fix MetadataLeafDecoder that returned bytes on empty. + + -- Scott Moser Fri, 17 Feb 2017 17:05:47 -0500 + +cloud-init (0.7.9-26-g1cd8cfa-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - apply the runtime configuration written by ds-identify. + + -- Scott Moser Tue, 14 Feb 2017 14:45:25 -0500 + +cloud-init (0.7.9-25-g65529b6-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - ds-identify: fix checking for filesystem label (LP: #1663735) + - ds-identify: read ds=nocloud properly (LP: #1663723) + + -- Scott Moser Fri, 10 Feb 2017 15:25:17 -0500 + +cloud-init (0.7.9-23-g0df21b6-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - support nova-lxd by reading platform from environment of pid 1. + (LP: #1661797) + - ds-identify: change aarch64 to use the default for non-dmi systems. + that default is to enable cloud-init even if no datasources are + explicitly found. + - Remove style checking during build and add latest style checks to tox + [Joshua Powers] (LP: #1652329) + - code-style: make master pass pycodestyle (2.3.1) cleanly. + + -- Scott Moser Thu, 09 Feb 2017 13:18:47 -0500 + +cloud-init (0.7.9-19-ge987092-0ubuntu2) zesty; urgency=medium + + * debian/update-grub-legacy-ec2: fix shell syntax error. (LP: #1662221) + + -- Scott Moser Mon, 06 Feb 2017 11:14:11 -0500 + +cloud-init (0.7.9-19-ge987092-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - manual_cache_clean: When manually cleaning touch a file in instance dir. + - Add tools/ds-identify to identify datasources available. + - Fix small typo and change iso-filename for consistency [Robin Naundorf] + - Fix eni rendering of multiple IPs per interface + [Ryan Harper] (LP: #1657940) + - tools/mock-meta: support python2 or python3 and ipv6 in both. + - tests: remove executable bit on test_net, so it runs, and fix it. + - tests: No longer monkey patch httpretty for python 3.4.2 + - Add 3 ecdsa-sha2-nistp* ssh key types now that they are standardized + [Lars Kellogg-Stedman] (LP: #1658174) + - reset httppretty for each test [Lars Kellogg-Stedman] (LP: #1658200) + - build: fix running Make on a branch with tags other than master + - EC2: Do not cache security credentials on disk + [Andrew Jorgensen] (LP: #1638312) + - doc: Fix typos and clarify some aspects of the part-handler + [Erik M. Bray] + - doc: add some documentation on OpenStack datasource. + - OpenStack: Use timeout and retries from config in get_data. + [Lars Kellogg-Stedman] (LP: #1657130) + - Fixed Misc issues related to VMware customization. [Sankar Tanguturi] + - Fix minor docs typo: perserve > preserve [Jeremy Bicha] + - Use dnf instead of yum when available [Lars Kellogg-Stedman] + (LP: #1647118) + - validate-yaml: use python rather than explicitly python3 + - Get early logging logged, including failures of cmdline url. + + -- Scott Moser Fri, 03 Feb 2017 21:54:39 -0500 + +cloud-init (0.7.9-0ubuntu2) zesty; urgency=medium + + * debian/copyright: update License field to include Apache. + * debian/update-grub-legacy-ec2: fix to include kernels whose config + has CONFIG_XEN=y (LP: #1379080). + + -- Scott Moser Fri, 20 Jan 2017 10:01:42 -0500 + +cloud-init (0.7.9-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - release 0.7.9 + - doc: adjust headers in tests documentation for consistency. + - integration test: initial commit of integration test framework + [Wesley Wiedenmeier] + - LICENSE: Allow dual licensing GPL-3 or Apache 2.0 [Jon Grimm] + - Fix config order of precedence, putting kernel command line over system. + [Wesley Wiedenmeier] (LP: #1582323) + - Update the list of valid ssh keys. [Michael Felt] + - network: add ENI unit test for statically rendered routes. + - set_hostname: avoid erroneously appending domain to fqdn + [Lars Kellogg-Stedman] (LP: #1647910) + - doc: change 'nobootwait' to 'nofail' in docs [Anhad Jai Singh] + - Replace an expired bit.ly link in code comment. + + -- Scott Moser Fri, 23 Dec 2016 12:54:50 -0500 + +cloud-init (0.7.8-68-gca3ae67-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - user-groups: fix bug when groups was provided as string and had spaces + (LP: #1354694) + + -- Scott Moser Mon, 19 Dec 2016 12:35:13 -0500 + +cloud-init (0.7.8-67-gc9c9197-0ubuntu1) zesty; urgency=medium + + * debian/cherry-pick: add utility for cherry picking commits from upstream + into patches in debian/patches. + * New upstream snapshot. + - mounts: use mount -a again to accomplish mounts (LP: #1647708) + - CloudSigma: Fix bug where datasource was not loaded in local search. (LP: #1648380) + - when adding a user, strip whitespace from group list (LP: #1354694) + - fix decoding of utf-8 chars in yaml test + - Replace usage of sys_netdev_info with read_sys_net (LP: #1625766) + - fix problems found in python2.6 test. + + -- Scott Moser Sun, 11 Dec 2016 21:22:57 -0500 + +cloud-init (0.7.8-61-g2d2ec70-0ubuntu1) zesty; urgency=medium + + * debian/grub-legacy-ec2.install: install post(inst|rm) files correctly. + [Simon Deziel] (LP: #1581416) + * New upstream snapshot. + - OpenStack: extend physical types to include hyperv, hw_veb, vhost_user. [Scott Moser] (LP: #1642679) + - tests: fix assumptions that expected no eth0 in system. [Scott Moser] (LP: #1644043) + - net/cmdline: Consider ip= or ip6= on command line not only ip= [Scott Moser] (LP: #1639930) + - Just use file logging by default [Joshua Harlow] (LP: #1643990) + - Improve formatting for ProcessExecutionError [Wesley Wiedenmeier] + - flake8: fix trailing white space [Scott Moser] + - Doc: various documentation fixes [Sean Bright] + - cloudinit/config/cc_rh_subscription.py: Remove repos before adding [Brent Baude] + - packages/redhat: fix rpm spec file. [Scott Moser] + - main: set TZ in environment if not already set. [Ryan Harper] + - Azure: No longer rely on walinux agent. [Scott Moser] (LP: #1538522) + - disk_setup: Use sectors as unit when formatting MBR disks with sfdisk. [Daniel Watkins] (LP: #1460715) + + -- Scott Moser Mon, 28 Nov 2016 16:08:09 -0500 + +cloud-init (0.7.8-49-g9e904bb-0ubuntu1) zesty; urgency=medium + + * debian/cloud-init.postinst: update /etc/fstab on Azure to fix + future resize operations. (LP: #1611074) + * New upstream snapshot. + - Add activate_datasource, for datasource specific code paths. + Use that on Azure to handle re-formatting of ephemeral disk. + (LP: #1611074) + + -- Scott Moser Fri, 18 Nov 2016 16:37:34 -0500 + +cloud-init (0.7.8-48-g9d826b8-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - systemd: cloud-init-local use RequiresMountsFor=/var/lib/cloud + (LP: #1642062) + - systemd: cloud-init remove After=systemd-networkd-wait-online + - systemd: cloud-init-local change Before basic to sysinit + + -- Scott Moser Tue, 15 Nov 2016 17:21:34 -0500 + +cloud-init (0.7.8-45-g584b843-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - pep8: fix style errors reported by pycodestyle 2.1.0 [Scott Moser] + - systemd: drop both Wants and After local-fs.target [Scott Moser] + - systemd: networking service adjustments. (LP: #1636912) + - systemd: replace Before=basic.target, dbus.target with sysinit.target + (LP: #1629797) + - doc: Add documentation on stages of boot. + - doc: make the RST files consistently formated and other improvements. + - Ec2: fix syntax and tox in previous commit. + - Ec2: protect against non-dictionary in block-device-mapping. + - doc: fixed example to not overwrite /etc/hosts [Chris Glass] + - Doc: fix spelling / typos in ca_certs and scripts_vendor. + + -- Scott Moser Thu, 10 Nov 2016 21:04:09 -0500 + +cloud-init (0.7.8-35-gc24187e-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - pyflakes: fix issue with pyflakes 1.3 found in ubuntu zesty-proposed. + + -- Scott Moser Mon, 07 Nov 2016 13:31:30 -0500 + +cloud-init (0.7.8-34-ga1cdebd-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - net/cmdline: Further adjustments to ipv6 support [LaMont Jones] + (LP: #1621615) + - Add coverage dependency to bddeb to fix package build. + - doc: improve HACKING.rst file + - dmidecode: Allow dmidecode to be used on aarch64 [Robert Schweikert] + - AliYun: Add new datasource for Ali-Cloud ECS [kaihuan.pkh] + - Add coverage collection to tox unit tests. [Joshua Powers] + + -- Scott Moser Fri, 04 Nov 2016 11:03:46 -0400 + +cloud-init (0.7.8-28-gf7a5756-0ubuntu1) zesty; urgency=medium + + * New upstream snapshot. + - cc_users_groups: fix remaing call to ds.normalize_user_groups + [Ryan Harper] + + -- Scott Moser Wed, 26 Oct 2016 09:06:03 -0400 + +cloud-init (0.7.8-27-g29348af-0ubuntu1) zesty; urgency=medium + + * debian/cloud-init.templates: enable DigitalOcean by default [Ben Howard] + * New upstream snapshot. + - disk-config: udev settle after partitioning in gpt format. (LP: #1626243) + - unittests: do not read system /etc/cloud/cloud.cfg.d (LP: #1635350) + - Add documentation for logging features. [Wesley Wiedenmeier] + - Add support for snap create-user on Ubuntu Core images. [Ryan Harper] + (LP: #1619393) + - Fix sshd restarts for rhel distros. [Jim Gorz] (LP: #1470433) + - OpenNebula: replace 'ip' parsing with cloudinit.net usage. + - Fix python2.6 things found running in centos 6. + - Move user/group functions to new ug_util file [Joshua Harlow] + - DigitalOcean: enable usage of data source by default. + - update Gentoo initscripts to run in the correct order [Matthew Thode] + - MAAS: improve the main of datasource to look at kernel cmdline config. + - tests: silence the Cheetah UserWarning about NameMapper C version. + + -- Scott Moser Tue, 25 Oct 2016 17:06:59 -0400 + +cloud-init (0.7.8-15-g6e45ffb-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - systemd: Run cloud-init.service Before dbus.socket not dbus.target + [Daniel Watkins] (LP: #1629797). + + -- Scott Moser Fri, 07 Oct 2016 12:41:38 -0400 + +cloud-init (0.7.8-14-g94fd35e-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - systemd: run cloud-init.service Before dbus.service (LP: #1629797) + - unittests: fix use of mock 2.0 'assert_called' when running make check + [Ryan Harper] + - Improve module documentation and doc cleanup. [Wesley Wiedenmeier] + + -- Scott Moser Tue, 04 Oct 2016 16:46:05 -0400 + +cloud-init (0.7.8-11-g02f6c4b-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - lxd: Update network config for LXD 2.3 [Stéphane Graber] + - DigitalOcean: use meta-data for network configruation [Ben Howard] + - ntp: move to run after apt configuration (LP: #1628337) + + -- Scott Moser Thu, 29 Sep 2016 14:30:15 -0400 + +cloud-init (0.7.8-8-g0439d8a-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - Decode unicode types in decode_binary [Robert Schweikert] + - systemd: Ensure that cloud-init-local happens before NetworkManager + - Allow ephemeral drive to be unpartitioned [Paul Meyer] + - subp: add 'update_env' argument + + -- Scott Moser Fri, 23 Sep 2016 16:51:27 -0400 + +cloud-init (0.7.8-4-g970dbd1-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - net: support reading ipv6 dhcp config from initramfs [LaMont Jones] + (LP: #1621615) + + -- Scott Moser Wed, 21 Sep 2016 11:56:23 -0400 + +cloud-init (0.7.8-3-g80f5ec4-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - Adjust mounts and disk configuration for systemd. (LP: #1611074) + - dmidecode: run dmidecode only on i?86 or x86_64 arch. [Robert Schweikert] + + -- Scott Moser Tue, 20 Sep 2016 13:59:20 -0400 + +cloud-init (0.7.8-1-g3705bb5-0ubuntu1) yakkety; urgency=medium + + * New upstream release 0.7.8. + * New upstream snapshot. + - systemd: put cloud-init.target After multi-user.target (LP: #1623868) + + -- Scott Moser Thu, 15 Sep 2016 09:47:11 -0400 + +cloud-init (0.7.7-31-g65ace7b-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - Allow link type of null in network_data.json [Jon Grimm] (LP: #1621968) + - DataSourceOVF: fix user-data as base64 with python3 (LP: #1619394) + + -- Scott Moser Mon, 12 Sep 2016 14:56:42 -0400 + +cloud-init (0.7.7-28-g34a26f7-0ubuntu2) yakkety; urgency=medium + + * debian/control: fix missing dependency on python3-serial, + and make SmartOS datasource work. + + -- Scott Moser Fri, 09 Sep 2016 21:22:13 -0400 + +cloud-init (0.7.7-28-g34a26f7-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - systemd: Better support package and upgrade. + (LP: #1576692, #1621336) + - tests: cleanup tempdirs in apt_source tests + + -- Scott Moser Fri, 09 Sep 2016 16:01:13 -0400 + +cloud-init (0.7.7-26-g058dd75-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - apt config conversion: treat empty string as not provided. (LP: #1621180) + - Fix typo in default keys for phone_home [Roland Sommer] (LP: #1607810) + - salt minion: update default pki directory for newer salt minion. + (LP: #1609899) + - bddeb: add --release flag to specify the release in changelog. + + -- Scott Moser Thu, 08 Sep 2016 09:36:52 -0400 + +cloud-init (0.7.7-22-g763f403-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - apt-config: allow both old and new format to be present. + [Christian Ehrhardt] (LP: #1616831) + - python2.6: fix dict comprehension usage in _lsb_release. [Joshua Harlow] + - Add a module that can configure spacewalk. [Joshua Harlow] + - add install option for openrc [Matthew Thode] + - Generate a dummy bond name for OpenStack (LP: #1605749) + * debian/control, debian/README.source: update to reference git. + * debian/new-upstream-snapshot: small fixes. + + -- Scott Moser Fri, 26 Aug 2016 15:22:13 -0400 + +cloud-init (0.7.7-17-g40a2f62-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - network: fix get_interface_mac for bond slave, read_sys_net for ENOTDIR + - azure dhclient-hook cleanups + - Minor cleanups to atomic_helper and add unit tests. + - Fix Gentoo net config generation [Matthew Thode] + * debian/control: drop very old meta-package ec2-init + * debian/rules, debian/cloud-init.install: remove install file + and use --destdir=debian/cloud-init. (LP: #1615745) + * debian/dirs: remove obsolete / unused file. + + -- Scott Moser Mon, 22 Aug 2016 17:02:56 -0400 + +cloud-init (0.7.7-13-g41271bd-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - distros: fix get_primary_arch method use of os.uname [Andrew Jorgensen] + - Apt: add new apt configuration format [Christian Ehrhardt] + - Get Azure endpoint server from DHCP client [Brent Baude] + * debian/new-upstream-snapshot: update to use git and packaging branches. + + -- Scott Moser Thu, 18 Aug 2016 17:11:51 -0400 + +cloud-init (0.7.7-10-gbc2c326-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot: + - DigitalOcean: use the v1.json endpoint + - MAAS: add vendor-data support (LP: #1612313) + - Upgrade the minimum configobj package to one new enough to work + - ConfigDrive: recognize 'tap' as a link type. (LP: #1610784) + - NoCloud: fix bug providing network-interfaces via meta-data. + (LP: #1577982) + - Add distro tags on config modules that should have it + - add ntp config module + - SmartOS: more improvements for network configuration + + -- Scott Moser Fri, 12 Aug 2016 17:18:01 -0400 + +cloud-init (0.7.7-0ubuntu1) yakkety; urgency=medium + + * New Upstream release. + + -- Scott Moser Wed, 10 Aug 2016 01:48:32 -0400 + +cloud-init (0.7.7~git1029-gb56d7a1-0ubuntu1) yakkety; urgency=medium + + * New Upstream snapshot. + - mcollective: add tests, cleanups and bug fix when no config in /etc. + - Move upstream revision control to git. [Lars Kellogg-Stedman] + - Adjust unit tests to address newer requests have strong type validation. + * debian/control: update Standards-Version to 3.9.8 + * debian/rules: fix PYVER in environment to sync with upstream changes. + + -- Scott Moser Tue, 09 Aug 2016 01:16:28 -0400 + +cloud-init (0.7.7~bzr1256-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - distros/debian.py: fix eni renderer to not render .link files + - fixes for execution in python2.6. + - ConfigDrive: fix writing of 'injected' files and legacy networking + (LP: #1602373) + - improvements to /etc/network/interfaces rendering including rendering + of 'lo' devices and sorting attributes within a interface section. + - fix mcollective module that was completely broken if using python3 + (LP: #1597699) + + -- Scott Moser Thu, 14 Jul 2016 14:54:05 -0400 + +cloud-init (0.7.7~bzr1246-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - fix restoring from a datasource that did not have dsmode (LP: #1596690) + + -- Scott Moser Mon, 27 Jun 2016 16:28:39 -0400 + +cloud-init (0.7.7~bzr1245-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - user_data: fix error when user-data is not utf-8 decodable (LP: #1532072) + - write_files: if no permissions are provided, use the default without + logging a warning. + + -- Scott Moser Wed, 22 Jun 2016 15:11:33 -0400 + +cloud-init (0.7.7~bzr1243-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - do not render systemd.link files (LP: #1594546) + + -- Scott Moser Mon, 20 Jun 2016 17:14:34 -0400 + +cloud-init (0.7.7~bzr1242-0ubuntu1) yakkety; urgency=medium + + * d/control: Build-Depends on python3-unittest2 + * New upstream snapshot. + - DataSourceNoCloud: fix stack trace on reboot, default to dsmode=net + (LP: #1592505) + - support network rendering to sysconfig (for centos and RHEL) + - fix errors reported by pylint + - move 'main' into cloudinit.cmd for easier testing. use + setuptools entry_points for creating executable. + - Remove trailing dot from GCE metadata URL (LP: #1581200) + - Change missing Cheetah log warning to debug [Andrew Jorgensen] + - make networking config provided in system config override datasource. + (LP: #1590104) + + -- Scott Moser Thu, 16 Jun 2016 00:07:12 -0400 + +cloud-init (0.7.7~bzr1227-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - fix one more unit test to run inside buildd. + + -- Scott Moser Sat, 04 Jun 2016 20:55:07 -0400 + +cloud-init (0.7.7~bzr1226-0ubuntu1) yakkety; urgency=medium + + * New upstream snapshot. + - fix unit tests to run inside buildd. + + -- Scott Moser Fri, 03 Jun 2016 23:01:16 -0400 + +cloud-init (0.7.7~bzr1225-0ubuntu1) yakkety; urgency=medium + + * debian/cloud-init.templates fix capitalisation in template so + dpkg-reconfigure works to select OpenStack. (LP: #1575727) + * New upstream snapshot. + - improve network configuration. Support DreamCompute, RDO openstack + and SmartOS networking + - remove blocking udev rules (LP: #1577844, LP: #1571761) + - support for renaming devices in a container (LP: #1579130). + - Apt sources configuration improvements (LP: #1574113) + - kernel command line: override all local settings (LP: #1582323) + - fix timestamp in reporting events. + + -- Scott Moser Fri, 03 Jun 2016 16:38:55 -0400 + +cloud-init (0.7.7~bzr1215-0ubuntu1) yakkety; urgency=medium + + * debian/new-upstream-snapshot: minor change supporting revision + passed in as an argument. + * New upstream snapshot. + - Paths: fix instance path if datasource's id has a '/'. (LP: #1575938) + - Config Drive: fix check_instance_id signature. (LP: #1575055) + - cloudstack: Only use DHCPv4 lease files as a datasource (LP: #1576273) + + -- Scott Moser Fri, 29 Apr 2016 12:37:48 -0400 + +cloud-init (0.7.7~bzr1212-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + - fix iscsi root by not writing interface as 'auto' when networking + information comes from kernel command line (LP: #1568637) + - apply networking less often, when possible only on first + instance boot (LP: #1571004). + - no longer delete /etc/network/interfaces.d/eth0.cfg on + ubuntu (LP: #1563487) + + -- Scott Moser Fri, 15 Apr 2016 16:25:43 -0400 + +cloud-init (0.7.7~bzr1209-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + - fallback net config: do not consider devices starting with + 'veth' (LP: #1569064) + + -- Scott Moser Thu, 14 Apr 2016 16:24:38 -0400 + +cloud-init (0.7.7~bzr1208-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + - phone_home: allow usage of fqdn (LP: #1566824) [Ollie Armstrong] + - chef: straighten out validation_cert and validation_key (LP: #1568940) + - skip bridges when generating fallback networking (LP: #1569974) + - rh_subscription: only check subscription if configured (LP: #1536706) + - SmartOS, CloudSigma: fix error when dmi data is not availble + (LP: #1569469) + - DataSourceNoCloud: fix check_instance_id when upgraded (LP: #1568150) + - lxd: adds basic support for dpkg based lxd-bridge + configuration. (LP: #1569018) + - centos: Ensure that a resolve conf object is written as a string. + (LP: #1479988) + + -- Scott Moser Wed, 13 Apr 2016 13:19:03 -0400 + +cloud-init (0.7.7~bzr1200-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + - fix adding of users when no group is specified (LP: #1562918) + - fix write_files with compressed content (LP: #1565638) + - systemd: do not specify After of obsolete syslog.target (LP: #1536964) + - chef: fix chef installation from gems (LP: #1553345) + - disk_setup: correctly send force to mkfs on block devices (LP: #1548772) + - locale: list unsupported environment settings in warning (LP: #1558069) + - fix NoCloud and other datasources if no datasource config (LP: #1514407) + + -- Scott Moser Wed, 06 Apr 2016 13:07:27 -0400 + +cloud-init (0.7.7~bzr1192-0ubuntu2) xenial; urgency=medium + + [Logan Rosen] + * debian/cloud-init.postinst: fix shell syntax in upgrade (LP: #1564187) + + -- Scott Moser Thu, 31 Mar 2016 10:09:09 -0400 + +cloud-init (0.7.7~bzr1192-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + - Misc fixes for VMware Support. + - network config improvements: + - do not raise exception on non-debian if network config is found. + - apply network config in 'cloud-init --local' even if there is + no datasource found. + - do not write 70-persistent-net.rules only systemd .link files. + - improve how cloud-init-wait waits: remove the check and sleep + loop and replace it with 'udevadm settle'. + + -- Scott Moser Tue, 29 Mar 2016 16:34:25 -0400 + +cloud-init (0.7.7~bzr1189-0ubuntu1) xenial; urgency=medium + + * d/control: adjust build dependency as python3-pyflakes in xenial + now provides python3 modules for pyflakes. + * d/cloud-init.install, d/control, d/dirs, d/grub-legacy-ec2.install: + run wrap-and-sort + * d/cloud-init.install: add lib/udev/ files. + * d/cloud-init.postinst: touch /var/lib/cloud/data/upgraded-network + if upgrading from version that did not have network support. + * New upstream snapshot. + * initial support for support network configuration in cloud-init --local + * do not rely on network metadata service on every boot for + ConfigDrive Openstack Azure (LP: #1553815) + + -- Scott Moser Thu, 24 Mar 2016 17:36:40 -0400 + +cloud-init (0.7.7~bzr1182-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + * systemd changes enforcing intended ordering (cloud-init-local.service + before networking and cloud-init.service before it comes up). + * when reading dmidecode data, return found but unset value as "" rather + than failing to decode that value. + * add default user to 'lxd' group and create groups when necessary + (LP: #1539317) + * No longer run pollinate in seed_random (LP: #1554152) + * Enable BigStep data source. + + -- Scott Moser Mon, 14 Mar 2016 09:58:56 -0400 + +cloud-init (0.7.7~bzr1176-0ubuntu1) xenial; urgency=medium + + * d/README.source, d/new-upstream-snapshot: simplify the README.source + with a script. + * d/rules: support DEB_BUILD_OPTIONS=nocheck and remove unused code. + * d/rules: make tests with python3 + * d/control: add pep8 as a build depends + * d/cloud-init.preinst, d/cloud-init.postinst adjust upgrade path + to adjust systemd jobs that put cloud-init unit jobs directly + in multi-user.target. + * New upstream snapshot. + * Add Image Customization Parser for VMware vSphere Hypervisor Support. + Disabled by default. [Sankar Tanguturi] + * lxd: add initial support for setting up lxd using 'lxd init' + * Handle escaped quotes in WALinuxAgentShim.find_endpoint (LP: #1488891) + * timezone: use a symlink when updating /etc/localtime (LP: #1543025) + * enable more code testing in 'make check' + * Added Bigstep datasource [Daniel Watkins] + * Enable password changing via a hashed string [Alex Sirbu] + + -- Scott Moser Fri, 04 Mar 2016 15:44:02 -0500 + +cloud-init (0.7.7~bzr1160-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + * SmartOS: Add support for Joyent LX-Brand Zones (LP: #1540965) + * systemd/power_state: fix power_state when cloud-final exited failure + (LP: #1449318) + + -- Scott Moser Thu, 04 Feb 2016 17:22:36 -0500 + +cloud-init (0.7.7~bzr1156-0ubuntu2) xenial; urgency=medium + + * debian/cloud-init.postinst, systemd_detect_virt.patch: Call + systemd-detect-virt instead of the Ubuntu specific running-in-container + wrapper. (LP: #1539016) + + -- Martin Pitt Thu, 28 Jan 2016 14:12:51 +0100 + +cloud-init (0.7.7~bzr1156-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + * d/cloud-init.preinst: migrate Azure instance ID from old ID to stable + ID (LP: #1506187). + + -- Ben Howard Tue, 17 Nov 2015 11:59:49 -0700 + +cloud-init (0.7.7~bzr1155-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + * cc_mounts: use 'nofail' rather than 'nobootwait' if system uses systemd. + (LP: #1514485). + + -- Scott Moser Tue, 10 Nov 2015 12:35:31 -0500 + +cloud-init (0.7.7~bzr1154-0ubuntu1) xenial; urgency=medium + + * New upstream snapshot. + * create the same /etc/apt/sources.list that is present in default server + ISO installs. This change adds restricted, multiverse, and -backports + (LP: #1177432). + + -- Scott Moser Thu, 05 Nov 2015 12:10:00 -0500 + +cloud-init (0.7.7~bzr1149-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + azure: support extracting SSH key values from ovf-env.xml (LP: #1506244) + + -- Scott Moser Wed, 14 Oct 2015 20:38:45 -0400 + +cloud-init (0.7.7~bzr1148-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * support configuring and installing the Ubuntu fan driver (LP: #1504604) + + -- Scott Moser Fri, 09 Oct 2015 13:00:12 -0400 + +cloud-init (0.7.7~bzr1147-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * MAAS: fix oauth when system clock is bad (LP: #1499869) + + -- Scott Moser Tue, 29 Sep 2015 20:16:57 -0400 + +cloud-init (0.7.7~bzr1146-0ubuntu2) wily; urgency=medium + + * replace usage of python with python3 in postinst (LP: #1498493) + + -- Scott Moser Tue, 22 Sep 2015 09:49:01 -0400 + +cloud-init (0.7.7~bzr1146-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * make the webhook reporter post json data rather than + urlencoded data (LP: #1496960) + + -- Scott Moser Thu, 17 Sep 2015 15:59:35 -0400 + +cloud-init (0.7.7~bzr1145-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * fix default builtin config of snappy to set ssh_enabled=auto. + + -- Scott Moser Tue, 15 Sep 2015 15:33:19 -0400 + +cloud-init (0.7.7~bzr1144-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + [ Ben Howard ] + * snappy: enable ssh on snappy if ssh keys are provided or + password authentication is requested (LP: #1494816) + + -- Scott Moser Fri, 11 Sep 2015 17:00:16 -0400 + +cloud-init (0.7.7~bzr1143-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * NoCloud: fix consumption of vendor-data (LP: #1493453) + * power_state: support power change only on 'condition' + + -- Scott Moser Thu, 10 Sep 2015 16:20:44 -0400 + +cloud-init (0.7.7~bzr1141-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * fix bug in cloud-init main preventing any usage. + + -- Scott Moser Thu, 03 Sep 2015 11:36:37 -0400 + +cloud-init (0.7.7~bzr1140-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * Handle symlink mount points in mount_cb (LP: #1490796). + * sync curtin reporting changes back to cloud-init. + + -- Scott Moser Wed, 02 Sep 2015 16:50:54 -0400 + +cloud-init (0.7.7~bzr1138-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * MAAS: fixes to data source and OauthUrlHelper (LP: #1488507) + + -- Scott Moser Tue, 25 Aug 2015 15:28:06 -0400 + +cloud-init (0.7.7~bzr1137-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * swap: use fallocate to create swapfile for speedup. (LP: #1482994) + * reporting: print handler fix. + + -- Scott Moser Wed, 12 Aug 2015 12:54:51 -0400 + +cloud-init (0.7.7~bzr1135-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * some stack traces fixed. + * improved rsyslog/syslog config format + * add reporting module for webhook or log reporting of events. + + -- Scott Moser Fri, 07 Aug 2015 17:16:51 -0400 + +cloud-init (0.7.7~bzr1127-0ubuntu1) wily; urgency=medium + + [ Scott Moser ] + * d/README.source, debian/cherry-pick-rev: improve packaging tool + + [ Daniel Watkins ] + * d/cloud-init.templates: Include SmartOS data source in the default list + and choices. (LP: #1398997) + + [ Scott Moser ] + * New upstream snapshot. + * check for systemd using sd_booted symantics (LP: #1461201) + * fix importing of gpg keys in python3 (LP: #1463373) + * fix specification of devices to growpart (LP: #1465436) + * reliably detect and use Azure disks using udev rules (LP: #1411582) + * support selection of Ubuntu mirrors on GCE (LP: #1470890) + * ssh: generate ed25519 host keys if supported (LP: #1461242) + * test fixes and cleanups + * fix reading of availability-zone on GCE (LP: #1470880) + * fix cloudsigma datasource with python3 (LP: #1475215) + * fix rightscale user-data + * fix consumption of CloudStack passwords on newer CloudStack platforms + (LP: #1440263, #1464253) + + -- Scott Moser Wed, 22 Jul 2015 17:06:18 -0400 + +cloud-init (0.7.7~bzr1109-0ubuntu2) wily; urgency=medium + + * d/control: cloud-init Recommends gdisk (LP: #1462521) + + -- Scott Moser Fri, 05 Jun 2015 16:33:38 -0400 + +cloud-init (0.7.7~bzr1109-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * Azure: Redact on-disk user password in /var/lib/ovf-env.xml + (LP: #1311827) + * EC2: be aware of eu-central-1 availability zone (LP: #1456684) + + -- Scott Moser Thu, 28 May 2015 10:54:45 -0400 + +cloud-init (0.7.7~bzr1106-0ubuntu1) wily; urgency=medium + + * New upstream snapshot. + * Azure: remove strict dependency on walinux-agent, but still utilize + it for the time being. + * fix read_seeded that is used in seeding user-data and meta-data + from additional locations (LP: #1455233) + * fix bug preventing partitioning of disks in many cases. (LP: #1311463) + * Azure: do not override hostname if user has set it (LP: #1375252) + * Fix GCE datasource not handling per-instance SSH keys (LP: #1403617) + * Allow specifying of uid in user/group config. + + -- Scott Moser Fri, 15 May 2015 17:04:19 -0400 + +cloud-init (0.7.7~bzr1091-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * fix processing of user-data in cloud-config-archive format (LP: #1445143) + + -- Scott Moser Fri, 17 Apr 2015 12:04:16 -0400 + +cloud-init (0.7.7~bzr1088-0ubuntu3) vivid; urgency=medium + + * systemd/*.service: don't declare a Wants/Requires on network.target; this + is a passive target that should only be pulled in by implementors of the + networking service. The requirement for network needs to be + expressed as a dependency on network-online.target. LP: #1440180. + + -- Steve Langasek Thu, 09 Apr 2015 07:35:55 -0700 + +cloud-init (0.7.7~bzr1088-0ubuntu2) vivid; urgency=medium + + [ Didier Roche ] + * Don't start or restart cloud-init services on install and upgrade + (LP: #1438520) + + [ Scott Moser ] + * d/control: Build-Depends on iproute2 (tests) + * d/control: Only Recommend (not both Depend and Recommend) + software-properties-common + + -- Scott Moser Fri, 03 Apr 2015 11:13:28 -0400 + +cloud-init (0.7.7~bzr1088-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * adjust cc_snappy for snappy install package with config. (LP: #1438836) + snappy install takes config as argument rather than '--config' flag. + + -- Scott Moser Tue, 31 Mar 2015 14:21:48 -0400 + +cloud-init (0.7.7~bzr1087-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * SmartOS: update ds to use v2 metadata. (LP: #1436417) [Daniel Watkins] + * Fix NoCloud local datasource to only activate if told to do so. + * fix snappy package installation. (LP: #1437137) + + -- Scott Moser Fri, 27 Mar 2015 17:09:34 -0400 + +cloud-init (0.7.7~bzr1084-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * systemd: update config and final to run even if init jobs fail + (LP: #1432758) + * emit_upstart: fix use of undeclared variable + * SmartOS: fixes for python3 reading from serial device. + + -- Scott Moser Tue, 17 Mar 2015 16:48:42 -0400 + +cloud-init (0.7.7~bzr1081-0ubuntu1) vivid; urgency=medium + + * fix failure of configure due to postinst usage of iteritems with python3 + * New upstream snapshot. + * better python3 handling of ignored binary mime parts + * DataSourceMAAS: fix usage of oauthlib and 'timestamp' + + -- Scott Moser Wed, 11 Mar 2015 13:52:04 -0400 + +cloud-init (0.7.7~bzr1078-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * run snappy module only on snappy (LP: #1428495) + * MAAS: adjust timestamp on oauthlib when needed (LP: #1427939) + + -- Scott Moser Thu, 05 Mar 2015 15:22:53 -0500 + +cloud-init (0.7.7~bzr1076-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * test case fixes for http_pretty + * python2.6 fixes + * Convert dmidecode values to sysfs names before looking. (LP: #1427687) + * add snappy support (LP: #1428139) + + -- Scott Moser Wed, 04 Mar 2015 17:13:55 -0500 + +cloud-init (0.7.7~bzr1072-0ubuntu1) vivid; urgency=medium + + * d/control: move software-properties-common to Recommends + this helps reduce snappy builds + * New upstream snapshot. + * fix MAAS datasource (LP: #1427263) + + -- Scott Moser Tue, 03 Mar 2015 20:46:41 -0500 + +cloud-init (0.7.7~bzr1067-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * fix broken consumption of gzipped user-data (LP: #1424900) + * functional user-data on Azure again (LP: #1423972) + * CloudStack: support fetching password from virtual router (LP: #1422388) + + -- Scott Moser Thu, 26 Feb 2015 14:19:16 -0500 + +cloud-init (0.7.7~bzr1060-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * Fix for ascii decode in DataSourceAzure (LP: #1422993). + + -- Scott Moser Fri, 20 Feb 2015 08:05:20 -0500 + +cloud-init (0.7.7~bzr1059-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * support for gpt partitioning, utilized in Azure [Daniel Watkins] + * fix bug in exception handling in mount_cb. + + -- Scott Moser Fri, 13 Feb 2015 16:05:59 -0500 + +cloud-init (0.7.7~bzr1055-0ubuntu1) vivid; urgency=medium + + * New upstream snapshot. + * move to python3 (LP: #1247132) + * systemd: run cloud-init before systemd-user-sessions.service + * Use the GCE short hostname. (LP: #1383794) + * Enable user-data encoding support for GCE. (LP: #1404311) + * Update to use a newer and better OMNIBUS_URL + * Be more tolerant of 'ssh_authorized_keys' types + * Fix parse_ssh_config failing in ssh_util.py + * Increase the robustness/configurability of the chef module. + * retain trailing newline from template files when using + jinja2 (LP: #1355343) + * fix broken output handling (LP: #1387340) + * digital ocean datasource + * update url in config drive documentation + * freebsd: enable correct behavior on Ec2. + * freebsd: Use the proper virtio FreeBSD network interface name. + + -- Scott Moser Wed, 11 Feb 2015 15:55:58 -0500 + +cloud-init (0.7.6~bzr1022-0ubuntu1) utopic; urgency=medium + + * New upstream snapshot. + * support for writing swap files per user config (disabled by default). + * fixes for freebsd support of config drive. + + -- Scott Moser Wed, 01 Oct 2014 16:27:44 -0400 + +cloud-init (0.7.6~bzr1020-0ubuntu1) utopic; urgency=medium + + [ Ben Howard ] + * Fix for cloud-init misidentifying grub install device (LP: #1336855). + + [ Scott Moser ] + * New upstream snapshot. + * cc_grub_dpkg: consider /dev/xvda as candidate for grub installation + (LP: #1336855) + * resizefs: fix backgrounding of resizefs (LP: #1338614) + * cloud-init-blocknet: remove debug code + + -- Scott Moser Tue, 23 Sep 2014 14:20:09 -0400 + +cloud-init (0.7.6~bzr1016-0ubuntu1) utopic; urgency=medium + + * New upstream snapshot. + * upstart: block networking from coming up until after + cloud-init-local has run. (LP: #1368861) + * upstart: ensure /run mounted before cloud-init-local (LP: #1353008) + * log failures in route info. + * Openstack: do not search redundant urls for openstack datasource. + do not load urls just to check if they're present when crawling. + * ConfigDrive: fixes for better consumption of vendor-data + * resizefs: make sure target is writable before attempting + in order to avoid failures in containers. (LP: #1366891) + * freebsd fixes. + * ssh_authkey_fingerprints: correctly allow disabling (LP: #1340903) + * CloudStack: work around bug in metadata service (LP: #1356855) + * docs: fix doc about disk-setup 'table_type' (LP: #1313114) + * resolv.conf: fix rendering if options not provided (LP: #1328953) + * ensure keys exist before running ssh on systemd (LP: #1333920) + + -- Scott Moser Fri, 12 Sep 2014 18:12:31 -0400 + +cloud-init (0.7.6~bzr992-0ubuntu1) utopic; urgency=medium + + * New upstream snapshot. + * ubuntu-init-switch: fixes and minor tweaks. + + -- Scott Moser Thu, 31 Jul 2014 16:11:27 -0400 + +cloud-init (0.7.6~bzr990-0ubuntu1) utopic; urgency=medium + + * debian/README.source: get changelog from trunk when new snapshot + * New upstream snapshot. + * add ubuntu-init-switch module for testing systemd. + * do not put comments in /etc/timezone (LP: #1341710) + + -- Scott Moser Thu, 31 Jul 2014 15:05:51 -0400 + +cloud-init (0.7.6~bzr987-0ubuntu2) utopic; urgency=medium + + * debian/control: drop build-depends on python-selinux as it is + not in main. + + -- Scott Moser Fri, 25 Jul 2014 12:47:39 -0400 + +cloud-init (0.7.6~bzr987-0ubuntu1) utopic; urgency=medium + + * incorporate systemd and build system changes from trunk. + * New upstream snapshot. + * less noisy logs + * allow usage of jinja2 templates (LP: #1219223) + * test case cleanups + * change build system to be dh and pybuild + + -- Scott Moser Thu, 24 Jul 2014 19:55:30 -0400 + +cloud-init (0.7.6~bzr976-0ubuntu1) utopic; urgency=medium + + * debian/cloud-init.templates: fix choices so dpkg-reconfigure works as + expected (LP: #1325746) + * New upstream snapshot. + * tests: SmartOS test not depend on /dev/ttyS1 device node (LP: #1316597) + * poll ttyS1 only after check for 'cloudsigma' in dmidecode (LP: #1316475) + * cloudsigma: support vendor-data (LP: #1303986) + + -- Scott Moser Tue, 03 Jun 2014 16:41:07 -0400 + +cloud-init (0.7.5-0ubuntu1) trusty; urgency=medium + + * New upstream release. + * support base64 encoded user-data in OpenNebula, required + to allow arbitrary content in user-data (LP: #1300941) + * pep8 and pylint fixes + + -- Scott Moser Tue, 01 Apr 2014 14:39:03 -0400 + +cloud-init (0.7.5~bzr970-0ubuntu1) trusty; urgency=medium + + * New upstream snapshot. + * fix NoCloud and seedfrom on the kernel command line (LP: #1295223) + + -- Scott Moser Thu, 20 Mar 2014 12:35:58 -0400 + +cloud-init (0.7.5~bzr969-0ubuntu1) trusty; urgency=medium + + * New upstream snapshot. + * Azure: Reformat ephemeral disk if it got re-provisioned + by the cloud on any reboot (LP: #1292648) + * final_message: fix replacement of upper case keynames (LP: #1286164) + * seed_random: do not capture output. Correctly provide + environment variable RANDOM_SEED_FILE to command. + * CloudSigma: support base64 encoded user-data + + -- Scott Moser Wed, 19 Mar 2014 14:04:34 -0400 + +cloud-init (0.7.5~bzr964-0ubuntu1) trusty; urgency=medium + + * New upstream snapshot. + * SmartOS, AltCloud: disable running on arm systems due to bug + (LP: #1243287, #1285686) [Oleg Strikov] + * Allow running a command to seed random, default is 'pollinate -q' + (LP: #1286316) [Dustin Kirkland] + * Write status to /run/cloud-init/status.json for consumption by + other programs (LP: #1284439) + * fix output of network information to not include 'addr:' (LP: #1285185) + + -- Scott Moser Mon, 03 Mar 2014 16:59:27 -0500 + +cloud-init (0.7.5~bzr952-0ubuntu1) trusty; urgency=medium + + * New upstream snapshot. + * fix broken seed of DAtaSourceNoCloud via external disk. + + -- Scott Moser Tue, 18 Feb 2014 14:10:52 -0500 + +cloud-init (0.7.5~bzr950-0ubuntu1) trusty; urgency=medium + + * New upstream snapshot. + * support for vendor-data in NoCloud + * fix in is_ipv4 to accept IP addresses with a '0' in them. + * Azure: fix issue when stale data in /var/lib/waagent (LP: #1269626) + * skip config_modules that declare themselves only verified on a set of + distros. Add them to 'unverified_modules' list to run anyway. + * Add CloudSigma datasource [Kiril Vladimiroff] + * Add initial support for Gentoo and Arch distributions [Nate House] + * Add GCE datasource [Vaidas Jablonskis] + * Add native Openstack datasource which reads openstack metadata + rather than relying on EC2 data in openstack metadata service. + + -- Scott Moser Fri, 14 Feb 2014 14:39:56 -0500 + +cloud-init (0.7.5~bzr933-0ubuntu1) trusty; urgency=medium + + * debian/control: bump Standards-Version to 3.9.5 + * debian/control: drop boto dependency no longer required in trunk. + * New upstream snapshot. + * ConfigDrive: consider partitions labelled correctly as possible sources. + * find root filesystem for resizing in cases where there is no initramfs + * removal of dependency on python-boto + * vendor-data support, and usage of that in Joyent datasource. + * change default output to be logged to /var/log/cloud-init-output.log + * SeLinuxGuard: Cast file path to string. (LP: #1260072) + * drop support for resizing via parted (LP: #1212492) + * SmartOS: changes to address changes in platform (LP: #1272115) + * FreeBSD support. + + -- Scott Moser Fri, 24 Jan 2014 22:41:57 -0500 + +cloud-init (0.7.5~bzr902-0ubuntu1) trusty; urgency=medium + + * debian/control: Build-Depend on python-jsonpatch as #717916 is now fixed. + * debian/control: Recommend eatmydata (LP: #1236531) + * New upstream snapshot. + * support invoking apt with 'eatmydata' (LP: #1236531) + * add a message in log about dynamic import failures + * New in '0.7.4' release. + * fix reading of mount information on kernels < 2.6.26 (LP: #1248625) + * SmartOS: change 'region' to 'datacenter_name' to address change + in data provided to instance (LP: #1249124) + * support calling 'add-apt-repository' for 'cloud-archive:' entries + (LP: #1244355) + * DataSourceAzure: fix incompatibility with python 2.6 (LP: #1232175) + * fix bug mounting first partition of a alias'd name. (LP: #1236594) + * SmartOS: fix bug with hostname due to trailing whitespace (LP: #1236445) + * fix creation of partitions on Azure (LP: #1233698) + * cc_growpart: respect /etc/growroot-disabled (LP: #1234331) + * ubuntu config: add default user to 'sudo' group (LP: #1228228) + * Fix usage of libselinux-python when selinux is disabled + * add OpenNebula datasource + + -- Scott Moser Tue, 17 Dec 2013 16:51:30 -0500 + +cloud-init (0.7.3-0ubuntu2) saucy; urgency=low + + * fix bug where a mount entry of 'ephemeral0' would only consider + the unpartitioned device, not also the first partition (LP: #1236594) + + -- Scott Moser Mon, 07 Oct 2013 20:16:02 -0400 + +cloud-init (0.7.3-0ubuntu1) saucy; urgency=low + + * New upstream release. + * Fix for SmartOS datasource when hostname is provided via dmi + data (LP: #1236445) + + -- Scott Moser Mon, 07 Oct 2013 14:49:56 -0400 + +cloud-init (0.7.3~bzr884-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * allow disabling of growpart via file /etc/growroot-disabled + (LP: #1234331) + * add default user to sudo group (LP: #1228228) + * fix disk creation on azure (LP: #1233698) + * DatasourceSmartOS: allow availabiltity-zone to be fed from the + datasource via 'region' (which allows 'mirrors' and other things + to make use of it). + + -- Scott Moser Fri, 04 Oct 2013 21:08:07 -0400 + +cloud-init (0.7.3~bzr879-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * fixes to disk_setup config module and enabling of partition + creation and filesystem creation on Azure. + + -- Scott Moser Fri, 27 Sep 2013 19:47:37 -0400 + +cloud-init (0.7.3~bzr871-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * add 'disk_setup' config module for partitioning disks and + creating filesystems. (LP: #1218506) + + -- Scott Moser Fri, 20 Sep 2013 20:46:08 -0400 + +cloud-init (0.7.3~bzr869-0ubuntu1) saucy; urgency=low + + * depend on cloud-utils or cloud-guest-utils (LP: #1224003) + * New upstream snapshot. + * Add OpenNebula datasource. + * Support reading 'random_seed' from metadata and writing to /dev/urandom + * fix for bug in log_time. + + -- Scott Moser Wed, 11 Sep 2013 17:04:45 -0400 + +cloud-init (0.7.3~bzr862-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * support base64 encoded data in the smart os datasource + + -- Scott Moser Thu, 29 Aug 2013 04:54:39 -0400 + +cloud-init (0.7.3~bzr861-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * fix publishing hostname on azure (LP: #1214541) + + -- Scott Moser Tue, 20 Aug 2013 16:06:22 -0400 + +cloud-init (0.7.3~bzr860-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * fix setting of password for a user on azure. (LP: #1212723) + + -- Scott Moser Thu, 15 Aug 2013 16:01:40 -0400 + +cloud-init (0.7.3~bzr858-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * fix resizing of root partition by preferring the functional 'growpart' + support over the broken 'parted resizepart' support (LP: #1212444) + * add options for apt_ftp_proxy, apt_https_proxy and apt_config + (LP: #1057195) + + -- Scott Moser Wed, 14 Aug 2013 21:44:22 -0400 + +cloud-init (0.7.3~bzr851-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * azure: do not wait for output of bouncing interface (ifdown; ifup) + as that waits on output of all ifupdown scripts to close all file + descriptors. + + -- Scott Moser Mon, 29 Jul 2013 12:21:08 -0400 + +cloud-init (0.7.3~bzr850-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * fix bouncing of interface. environment was not being modified + so command invoked did not have access to 'interface'. + * debian/README.source: update to read upstream version from trunk + + -- Scott Moser Fri, 26 Jul 2013 14:34:02 -0400 + +cloud-init (0.7.3~bzr849-0ubuntu2) saucy; urgency=low + + * debian/control: fix bad dependency on python-jsonpatch + by build-depending on python-json-patch, so dh_python2 + can find the right package (LP: #1205358). + + -- Scott Moser Fri, 26 Jul 2013 10:47:59 -0400 + +cloud-init (0.7.3~bzr849-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * azure: support publishing hostname via bouncing interface (LP: #1202758) + + -- Scott Moser Thu, 25 Jul 2013 17:08:30 -0400 + +cloud-init (0.7.3~bzr845-0ubuntu2) saucy; urgency=low + + * debian/control: fix dependency python-json-patch. + + -- Scott Moser Wed, 24 Jul 2013 15:01:24 -0400 + +cloud-init (0.7.3~bzr845-0ubuntu1) saucy; urgency=low + + * Reads the currently set value in /etc/cloud/cloud.cfg.d/90_dpkg.cfg to + db_set the value of cloud-init/datasources. (Closes: #709773) + * New upstream snapshot. + * azure: use deployment-id rather than static instance-id (LP: #1204190) + * config-drive: make 'availability_zone' available. (LP: #1190431) + * finalize handlers even on error (LP: #1203368) + * azure: fix password based access (LP: #1201969) + * add smartos (Joyent cloud) datasource + * support patching cloud-config via jsonp (LP: #1200476) + * debian/control: add dependency on python-jsonp + + -- Scott Moser Wed, 24 Jul 2013 13:47:53 -0400 + +cloud-init (0.7.3~bzr829-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * fix to upstart_job handler if version upstart is version 1.8. + * Azure datasource: allow userdata to be found in node named + 'UserData' or 'CustomData' + + -- Scott Moser Thu, 11 Jul 2013 10:20:03 -0400 + +cloud-init (0.7.3~bzr826-0ubuntu2) saucy; urgency=low + + * debian/cloud-init.templates: add 'Azure' datasource to list + of available datasources. + + -- Scott Moser Wed, 10 Jul 2013 16:31:48 -0400 + +cloud-init (0.7.3~bzr826-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * Fix omnibus support (LP: #1182265) + * invoke 'initctl reload-configuration' on upstart jobs again + (LP: #1124384) + * Remove unowned files in /etc/apt/apt.conf.d/ after purge. (Closes #674237) + + -- Scott Moser Wed, 10 Jul 2013 13:35:59 -0400 + +cloud-init (0.7.2-0ubuntu1) saucy; urgency=low + + * New upstream release. + * fix merging routines to be backwards compatible (LP: #1180867) + * fix for python 2.6 + + -- Scott Moser Wed, 05 Jun 2013 11:12:46 -0400 + +cloud-init (0.7.2~bzr812-0ubuntu1) saucy; urgency=low + + * New upstream snapshot. + * catch up with upstream, which is hopefully 0.7.2 + * straighten out the merging routines + * fix a bug in Maas datasource + + -- Scott Moser Fri, 10 May 2013 17:53:49 -0400 + +cloud-init (0.7.2~bzr809-0ubuntu1) raring; urgency=low + + * New upstream snapshot. + * make apt-get invoke 'dist-upgrade' rather than 'upgrade' for + package_upgrade. (LP: #1164147) + * workaround 2.6 kernel issue that stopped blkid from showing /dev/sr0 + + -- Scott Moser Thu, 11 Apr 2013 12:55:51 -0400 + +cloud-init (0.7.2~bzr804-0ubuntu1) raring; urgency=low + + * New upstream snapshot. + * use python-requests rather than urllib2 for http (LP: #1067888) + * handle failure of resizefs better. Specifically, do not show + warnings or stack trace in lxc (LP: #1160462) + + -- Scott Moser Wed, 27 Mar 2013 10:04:41 -0400 + +cloud-init (0.7.2~bzr795-0ubuntu1) raring; urgency=low + + * New upstream snapshot. + * documentation on write-files module (LP: #1111205) + * support for specifying package versions in package installs + * DataSourceNoCloud: allow specifyin user-data and meta-data in + the datasource config (LP: #1115833) + * work around bug in upstart for now (1124384) + * support resizing btrfs fileystems + * parse ssh keys more correctly (LP: #1136343) + * upstart/cloud-init-nonet.conf: handle sigterm gracefully (LP: #1015223) + * support growing partitions (LP: #1136936) + * use --force-unsafe-io for dpkg installations to improve speed + This is sane as it happens on instance initialization. + * more powerful and user-suppliable cloud-config merge mechanisms + (LP: #1023179) + + -- Scott Moser Thu, 07 Mar 2013 17:33:59 -0500 + +cloud-init (0.7.1-0ubuntu5) raring; urgency=low + + * catchup-751-760.patch (sync to 760) + * DataSourceConfigDrive: allow config-drive data to come from a + CD-ROM (LP: #1100545) + * Allow 'sr0' to be used as a source for mount config [Vlastimil Holer] + * do not log passwords provided via config (LP: #1096417) + * DataSourceCloudStack: Attempt to find the 'virtual router' as provided + from dhcp responses, rather than assuming it is the default route + (LP: #1089989) [Gerard Dethier] + in the CloudStack environment use virtual router rather than default route + * notify upstart after writing upstart jobs to support filesystems that + do not support inotify such as overlayfs (LP: #1080841) + * fix cloud-config 'lock_password' user setup (LP: #1096423) + * debian/README.source: minor improvements. + + -- Scott Moser Fri, 18 Jan 2013 10:12:34 -0500 + +cloud-init (0.7.1-0ubuntu4) raring; urgency=low + + * cherry pick relevant patches from trunk up to revision 750 + * use short form of '--stderr' argument to logger for better cross + distro support (LP: #1083715) + * puppet: make installation configurable (LP: #1090205) + * chef: add omnibus installation method + * fix allowing cloud-config input via user-data to affect the + apt mirror selection (LP: #090482) + + -- Scott Moser Mon, 17 Dec 2012 10:48:23 -0500 + +cloud-init (0.7.1-0ubuntu3) raring; urgency=low + + * cherry pick relevant patches from trunk up to revision 745 + * fix writing of sudoers when suders rule is a string rather than + an array (LP: #1079002) + * add trailing slash to sudoers files that are written + * fix resizefs module when 'noblock' was provided (LP: #1080985) + * make sure there is no blank line before cloud-init entry in + there are no blank lines in /etc/ca-certificates.conf (LP: #1077020) + + -- Scott Moser Mon, 03 Dec 2012 21:45:48 -0500 + +cloud-init (0.7.1-0ubuntu2) raring; urgency=low + + * debian/watch: add watch file + * add 'ubuntu' user to sudoers (LP: #1080717) + * set 'ubuntu' user shell to bash + + -- Scott Moser Mon, 19 Nov 2012 09:38:29 -0500 + +cloud-init (0.7.1-0ubuntu1) raring; urgency=low + + * New upstream release. + * landscape: install landscape-client package if not installed. + only take action if cloud-config is present (LP: #1066115) + * landscape: restart landscape after install or config (LP: #1070345) + * multipart/archive: do not fail on unknown headers in multipart + mime or cloud-archive config (LP: #1065116). + * tools/Z99-cloud-locale-test.sh: avoid warning when user's shell is + zsh (LP: #1073077) + * fix stack trace when unknown user-data input had unicode (LP: #1075756) + * split 'apt-update-upgrade' config module into 'apt-configure' and + 'package-update-upgrade-install'. The 'package-update-upgrade-install' + will be a cross distro module. + * fix bug where cloud-config from user-data could not affect system_info + settings (LP: #1076811) + * add yum_add_repo configuration module for adding additional yum repos + * fix public key importing with config-drive-v2 datasource (LP: #1077700) + * handle renaming and fixing up of marker names (LP: #1075980) + this relieves that burden from the distro/packaging. + * group config: fix how group members weren't being translated correctly + when the group: [member, member...] format was used (LP: #1077245) + * work around an issue with boto > 0.6.0 that lazy loaded the return from + get_instance_metadata(). This resulted in failure for cloud-init to + install ssh keys. (LP: #1068801) + * add power_state_change config module for shutting down stystem after + cloud-init finishes. (LP: #1064665) + + + -- Scott Moser Wed, 14 Nov 2012 15:18:50 -0500 + +cloud-init (0.7.0-0ubuntu2) quantal; urgency=low + + * config-drive: copy metadata entry 'hostname' to 'local-hostname' to fix + config modules (set_hostname) will function as expected (LP: #1061964) + + -- Scott Moser Fri, 05 Oct 2012 11:45:15 -0400 + +cloud-init (0.7.0-0ubuntu1) quantal; urgency=low + + * New upstream release. + * minor change to oauth header fix (LP: #978127). + * incorporation of 0.7.0 upstream release. + + -- Scott Moser Mon, 01 Oct 2012 14:19:46 -0400 + +cloud-init (0.7.0~bzr677-0ubuntu1) quantal; urgency=low + + * add CloudStack to DataSources listed by dpkg-reconfigure (LP: #1002155) + * New upstream snapshot. + * 0440 permissions on /etc/sudoers.d files rather than 0644 + * get host ssh keys to the console (LP: #1055688) + * MAAS DataSource adjust timestamp in oauth header to one based on the + timestamp in the response of a 403. This accounts for a bad local + clock. (LP: #978127) + * re-start the salt daemon rather than start to ensure config changes + are taken. + * allow for python unicode types in yaml that is loaded. + * cleanup in how config modules get at users and groups. + + -- Scott Moser Sun, 30 Sep 2012 14:29:04 -0400 + +cloud-init (0.7.0~bzr659-0ubuntu2) quantal; urgency=low + + * debian/cloud-init.templates: fix bad template file (LP: #1053239) + + -- Scott Moser Thu, 20 Sep 2012 09:18:20 -0400 + +cloud-init (0.7.0~bzr659-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * add signal handlers to more cleanly exit + * add logging fallback in case logging fails + * documentation fix for landscape config (LP: #1042764) + * do not write a comment in /etc/hostname (LP: #1052664) + * fix incorrect ubuntu mirrors for 'ports' arches [Robbie Basak] + * fix generation of hostname based on ip address in datasource + (LP: #1050962) [Andy Grimm] + * remove 'start networking' from cloud-init-nonet and replace it + with container specific fixes (LP: #1031065) + * fix landscape configuration so client will run (LP: #1042758) + * enable all available datasources (adding AltCloud and None) + * fix bug where user data scripts re-ran on upgrade from 10.04 versions + (LP: #1049146) + + -- Scott Moser Wed, 19 Sep 2012 22:08:51 -0400 + +cloud-init (0.7.0~bzr644-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * fix cloud-archives (LP: #1044594) + * fix set_passwords for usergroups as a list (LP: #1044553) + * fix 'failed to setup apt-pipelining' warning on install due to + old 0.6 style usage of cloud-init. + + -- Scott Moser Sat, 01 Sep 2012 20:11:34 -0400 + +cloud-init (0.7.0~bzr642-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * support using launch-index (ami-launch-index) (LP: #1023177) + * usergroup related fixes (LP: #1041384, #1044044, #1044508) + + -- Scott Moser Fri, 31 Aug 2012 17:04:06 -0400 + +cloud-init (0.7.0~bzr639-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * fix broken ssh_import_id, which would give stack trace + + -- Scott Moser Tue, 28 Aug 2012 14:09:47 -0400 + +cloud-init (0.7.0~bzr637-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * fix issue with public keys not being added to 'ubuntu' user + since the user was not created yet. (LP: #1042459) + * only search the top level domain 'instance-data' for the EC2 + metadata service, to avoid misconfiguration or unexpected results + by searching search entries in /etc/resolv.conf (LP: #1040200) + + -- Scott Moser Mon, 27 Aug 2012 20:27:06 -0400 + +cloud-init (0.7.0~bzr634-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * support for datasource from config-drive-v2 (LP: #1037567) + + -- Scott Moser Fri, 24 Aug 2012 17:24:26 -0400 + +cloud-init (0.7.0~bzr633-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * support creating users on boot. remove requirement for a 'ubuntu' + user to be previously present in image. (LP: #1028503) + * add experimental apt_reboot_if_required flag to reboot if necessary + after upgrade or package install (LP: #1038108) + * improve mirror selection for a distro: + * support arm mirrors (LP: #1028501) + * support seeding security mirror (LP: #1006963) + * support dns mirrors including availability-zone reference + (LP: #1037727) + * include a "None" datasource so items like ssh host key generation + occur if there is no other metadata service. (LP: #906669) + * print authorized_keys for users to the console (LP: #1010582) + * Add RHEVm and vSphere support as datasource AltCloud [Joseph VLcek] + + -- Scott Moser Thu, 23 Aug 2012 01:06:34 -0400 + +cloud-init (0.7.0~bzr614-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * disable searching for 'ubuntu-mirror' in local dns to find a + local mirror (LP: #974509) + * emit the cloud-config event (LP: #1028674) + * write timestamps to console on reboot and shutdown (LP: #1018554) + + -- Scott Moser Fri, 03 Aug 2012 14:55:37 -0400 + +cloud-init (0.7.0~bzr608-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * fix issue with EC2 datasource that prevented /mnt from being mounted. + + -- Scott Moser Mon, 16 Jul 2012 16:49:55 -0400 + +cloud-init (0.7.0~bzr604-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * add cc_write_file for injecting files via cloud-config (LP: #1012854) + * fix issue with empty user data + * remove some un-needed warnings to console output in DataSourceOVF + * allow user-data scripts output through to the console + + -- Scott Moser Thu, 12 Jul 2012 16:11:01 -0400 + +cloud-init (0.7.0~bzr583-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + * debian/control: wrap-and-sort + * debian/control: actually depend on software-properties-common + * debian/control: depend on python-cheetah again instead of python-tempita + + -- Scott Moser Mon, 09 Jul 2012 17:41:22 -0400 + +cloud-init (0.7.0~bzr564-0ubuntu2) quantal; urgency=low + + * debian/control: Build-Depends on python-setuptools (LP: #1022101) + + -- Angel Abad Sat, 07 Jul 2012 18:43:05 +0200 + +cloud-init (0.7.0~bzr564-0ubuntu1) quantal; urgency=low + + * New upstream snapshot. + Thanks to Joshua Harlow for hard work. + * depend on software-properties-common rather than + python-software-properties (LP: #1021418) + + -- Scott Moser Fri, 06 Jul 2012 17:31:01 -0400 + +cloud-init (0.6.3-0ubuntu3) quantal; urgency=low + + * grub-legacy-ec2: add missing dependency on ucf (LP: #960336). + + -- Robie Basak Sun, 24 Jun 2012 05:10:13 +0100 + +cloud-init (0.6.3-0ubuntu2) quantal; urgency=high + + * Added -generic to Xen kernels list since -virtual has been dropped with + Quantal. (LP: #1005551) + + -- Ben Howard Tue, 29 May 2012 12:59:01 -0600 + +cloud-init (0.6.3-0ubuntu1) precise; urgency=low + + * New upstream release. + * improve chef examples for working configurations on 11.10 and 12.04 + [Lorin Hochstein] (LP: #960564) + * fix bug in landscape module if /etc/landscape did not exist + (LP: #978329) + + -- Scott Moser Wed, 11 Apr 2012 00:05:00 -0400 + +cloud-init (0.6.3~bzr554-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + * Fix bug in Chef support that required 'validation_cert' (LP: #960547) + * Provide user-friendly message when a user ssh's in with an invalid + locale (LP: #960547) + * Support reading a url reference to cloud-config from the kernel + command line. + + -- Scott Moser Thu, 05 Apr 2012 01:24:42 -0400 + +cloud-init (0.6.3~bzr551-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + * support running resize2fs in the background (default=off) (LP: #961226) + + -- Scott Moser Thu, 22 Mar 2012 14:33:59 -0400 + +cloud-init (0.6.3~bzr548-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + * If public-keys is a string, split it into multiple keys on newline + This specifically helps the MAAS data source, and should not negatively + affect others. + + -- Scott Moser Mon, 19 Mar 2012 13:50:50 -0400 + +cloud-init (0.6.3~bzr547-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + * rename DataSourceMaaS to DataSourceMAAS. + * support public-keys in DataSourceMAAS + * Warn in user-data processing on non-multipart, non-handled data + * CloudStack data source added (not enabled by default) + * fix bug in cloud-init.postinst where the name used was wrong + causing config-apt-pipelining to run more than intended + + -- Scott Moser Fri, 16 Mar 2012 14:12:38 -0400 + +cloud-init (0.6.3~bzr539-0ubuntu3) precise; urgency=low + + * make maas config file only readable by root (LP: #954721) + + -- Scott Moser Wed, 14 Mar 2012 01:19:32 -0400 + +cloud-init (0.6.3~bzr539-0ubuntu2) precise; urgency=low + + [Cosmin Luta] + * add dependency on python-oauth (LP: #953915) + + -- Scott Moser Tue, 13 Mar 2012 11:36:11 -0400 + +cloud-init (0.6.3~bzr539-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + * add ability to configure Acquire::http::Pipeline-Depth via + cloud-config setting 'apt_pipelining' (LP: #942061) + * if cloud-config settings removed default certificats + (remove-defaults), then seed package ca-certificates to not + install new ones on upgrade. + * run-parts now uses internal implementation rather than + separate command. + * add MaaS datasource (LP: #942061) + * debian/cloud-init.postinst: address population of apt_pipeline + setting on installation. + * debian/cloud-init.postinst: support configuring cloud-init + maas datasource via preseed values cloud-init/maas-metadata-url and + cloud-init/maas-credentials. (LP: #942061) + * debian/cloud-init.postinst: support for (LP: #924375) + + -- Scott Moser Fri, 09 Mar 2012 16:37:01 -0500 + +cloud-init (0.6.3~bzr530-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + - fix DataSourceNoCloud seeded from local or cmdline (LP: #942695) + - change 'islxc' to 'iscontainer' and use 'running-in-container' utility + from upstart rather than 'lxc-is-container' (LP: #941955) + - Do not fail on bad part handlers, instead catch error and log + + -- Scott Moser Tue, 28 Feb 2012 19:15:19 -0500 + +cloud-init (0.6.3~bzr527-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + - exit 0 in cloud-init if no metadata is found (nothing to do) + - documentation improvements + - support network config in DataSourceNoCloud + + -- Scott Moser Fri, 17 Feb 2012 17:11:50 -0500 + +cloud-init (0.6.3~bzr519-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + - [Mike Milner] add support for managing CA Certificates (LP: #915232) + - in ci-info lines, use '.' to for empty field for easier machine reading + - support empty lines in "#include" files (LP: #923043) + - [Jef Baeur] support configuration of salt minions Bauer) (LP: #927795) + - DataSourceOVF: only search for OVF data on ISO9660 filesystems (LP: #898373) + - DataSourceConfigDrive: support getting data from openstack config drive + (LP: #857378) + - [Juerg Haefliger] formating and pylint cleanups + * increase timeouts for initial config check for metadata service + to address slow metadata service in openstack + * add awareness of ConfigDrive data source + + -- Scott Moser Thu, 16 Feb 2012 17:27:05 -0500 + +cloud-init (0.6.3~bzr502-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + - [Mike Milner] add test case framework (LP: #890851) + - [Juerg Haefliger] fix pylint warnings (LP: #914739) + - fix regression where ec2 mirrors were not selected (LP: #915282) + + -- Scott Moser Thu, 12 Jan 2012 17:56:52 +0100 + +cloud-init (0.6.3~bzr497-0ubuntu1) precise; urgency=low + + * New upstream snapshot. + - cloud-config support for configuring apt-proxy + - selection of local mirror based on presense of 'ubuntu-mirror' dns + entry in local domain. (LP: #897688) + - DataSourceEc2: more resilliant to slow metadata service (LP: #894279) + - close stdin in all programs launched by cloud-init (LP: #903993) + - revert management of /etc/hosts to 0.6.1 style (LP: #890501, LP: #871966) + - write full ssh keys to console for easy machine consumption (LP: #893400) + - put INSTANCE_ID environment variable in bootcmd scripts + - add 'cloud-init-per' script for easily running things with a given freq + (this replaced cloud-init-run-module) + - support configuration of landscape-client via cloud-config (LP: #857366) + - part-handlers now get base64 decoded content rather than 2xbase64 encoded + in the payload parameter. (LP: #874342) + + -- Scott Moser Thu, 22 Dec 2011 04:07:38 -0500 + +cloud-init (0.6.2-0ubuntu2) precise; urgency=low + + * Build using dh_python2. LP: #904248. + * debian/rules: Explicitly set DEB_PYTHON2_MODULE_PACKAGES = cloud-init. + + -- Matthias Klose Sat, 17 Dec 2011 21:08:23 +0000 + +cloud-init (0.6.2-0ubuntu1) precise; urgency=low + + * New upstream release + + -- Scott Moser Thu, 27 Oct 2011 23:05:15 -0400 + +cloud-init (0.6.1-0ubuntu20) oneiric; urgency=low + + * fix broken path if local-hostname was not in metadata (LP: #857380) + * redirect output of 'start networking' in 'cloud-init-nonet' to /dev/null + * include GPLv3 in source tree + + -- Scott Moser Fri, 23 Sep 2011 09:24:27 -0400 + +cloud-init (0.6.1-0ubuntu19) oneiric; urgency=low + + * If local-hostname is not in meta-data, attempt to look up hostname in + an alias in /etc/hosts. This will avoid setting domain portion of fqdn + to 'localdomain' in some cases (LP: #850206). + + -- Scott Moser Wed, 14 Sep 2011 15:15:00 -0400 + +cloud-init (0.6.1-0ubuntu18) oneiric; urgency=low + + * minor documentation improvement. + + [Mike Moulton, Avishai Ish-Shalom] + * Chef support fixes. support for environment and initial attr (LP: #845208) + + -- Scott Moser Tue, 13 Sep 2011 17:02:48 -0400 + +cloud-init (0.6.1-0ubuntu17) oneiric; urgency=low + + * fix issues with chef (LP: #845161) + * be more forgiving on metadata for public-keys (LP: #845155) + + -- Scott Moser Fri, 09 Sep 2011 14:19:03 -0700 + +cloud-init (0.6.1-0ubuntu16) oneiric; urgency=low + + * catch up with trunk at revision 439 + * warn on failure to set hostname (LP: #832175) + * properly wait for all static interfaces to be up before + cloud-init runs (depends on fix in LP:# 838968). + * in DataSources NoCloud and OVF, do not set hostname to the + static value 'ubuntuhost' if local-hostname is not in metadata + (LP: #838280) + * improve the way ssh_authorized_keys is updated, so that the + values given will be used. (LP: #434076, LP: #833499) + * cloud-init-notnet.conf: minor changes to config + + -- Scott Moser Thu, 01 Sep 2011 21:14:09 -0400 + +cloud-init (0.6.1-0ubuntu15) oneiric; urgency=low + + * catch up with trunk at revision 431 + * add network debug info to console when cloud-init runs (LP: #828186) + * fix issue where subprocesses (apt-add-repository) where given + the console and would attempt to prompt user and hang boot (LP: #831505) + * add awareness of ecdsa to cc_ssh + + -- Scott Moser Tue, 23 Aug 2011 00:01:01 -0400 + +cloud-init (0.6.1-0ubuntu14) oneiric; urgency=low + + * change the handling of user-data (LP: #810044) + * boothooks will now run more than once as they were intended + * cloud-config and user-scripts will be updated from user data every boot + * Add a second type of part-handler that will be called on every boot + * fix bad handling of /etc/hosts if manage_etc_hosts was false + + -- Scott Moser Mon, 08 Aug 2011 12:46:56 -0500 + +cloud-init (0.6.1-0ubuntu13) oneiric; urgency=low + + * do not install 92-uec-upgrade-available as a motd hook. This file was + installed but did not do anything since updates-check was removed. + * support multiple staticly configured network devices, as long as + all of them come up early (LP: #810044) + + [Marc Cluet] + * add support for passing mcollective keys via cloud-config + * add support for 'include-once' type. include-once urls are only + retrieved once-per-instance rather than on every boot. + + -- Scott Moser Mon, 01 Aug 2011 16:45:40 -0400 + +cloud-init (0.6.1-0ubuntu12) oneiric; urgency=low + + * do not give trace on failure to resize in lxc container (LP: #800856) + * increase the timeout on url gets for "seedfrom" values (LP: #812646) + * do not write entries for ephemeral0 on t1.micro (LP: #744019) + + [Adam Gandalman] + * improve the updating of /etc/hosts with correct fqdn when possible + (LP: #812539) + + [Avishai Ish-Shalom] + * add chef support (cloudinit/CloudConfig/cc_chef.py) (LP: #798844) + + -- Scott Moser Thu, 21 Jul 2011 05:51:03 -0400 + +cloud-init (0.6.1-0ubuntu11) oneiric; urgency=low + + [Marc Cluet] + * sanitize hosts file for system's hostname to 127.0.1.1 (LP: #802637) + + -- Scott Moser Thu, 30 Jun 2011 14:12:47 -0400 + +cloud-init (0.6.1-0ubuntu10) oneiric; urgency=low + + * sync with trunk (rev 405) + * fix cloud-init in ubuntu lxc containers (LP: #800824) + + -- Scott Moser Tue, 28 Jun 2011 06:42:45 -0400 + +cloud-init (0.6.1-0ubuntu9) oneiric; urgency=low + + * sync with trunk (rev 404) + * make metadata urls configurable, to support eucalyptus in + STATIC or SYSTEM modes (LP: #761847) + * support disabling byobu in cloud-config (LP: #797336) + * guarantee that ssh_config runs before sshd starts (LP: #781101) + * make prefix for keys added to /root/.ssh/authorized_keys configurable + and add 'no-port-forwarding,no-agent-forwarding,no-X11-forwarding' + to the default (LP: #798505) + * make 'cloud-config ready' command configurable (LP: #785551) + * make fstab fields used to 'fill in' shorthand mount entries configurable + (LP: #785542) + * read sshd_config to properly get path for authorized_keys (LP: #731849) + + -- Scott Moser Fri, 17 Jun 2011 12:18:34 -0400 + +cloud-init (0.6.1-0ubuntu8) natty; urgency=low + + * instead of including /boot/grub, create it in postinst + of grub-legacy-ec2. + + -- Scott Moser Fri, 15 Apr 2011 13:01:17 -0400 + +cloud-init (0.6.1-0ubuntu7) natty; urgency=low + + * grub-legacy-ec2: add /boot/grub directory so installation does + not depend on it already existing (LP: #759885) + + -- Scott Moser Wed, 13 Apr 2011 11:03:04 -0400 + +cloud-init (0.6.1-0ubuntu6) natty; urgency=low + + * avoid upgrade prompt for grub-pc when devices are named xvdX (LP: #752361) + * catchup to trunk cloud-init (rev 395) + + -- Scott Moser Wed, 06 Apr 2011 06:46:55 -0400 + +cloud-init (0.6.1-0ubuntu5) natty; urgency=low + + * fix --purge of grub-legacy-ec2 package (LP: #749444) + * catchup to trunk cloud-init (rev 394) + * support user-data formated in dos format by converting to unix for + user-scripts, boothooks, and upstart jobs (LP: #744965) + * removal of some debug code, minor documentation fix + + -- Scott Moser Mon, 04 Apr 2011 13:20:27 -0400 + +cloud-init (0.6.1-0ubuntu4) natty; urgency=low + + * catch up to trunk cloud-init (rev 389). + * fix bug in part-handler code, that broke part handlers (LP: #739694) + * fix sporadic resizefs failure (LP: #726938) + + -- Scott Moser Mon, 21 Mar 2011 22:06:59 -0400 + +cloud-init (0.6.1-0ubuntu3) natty; urgency=low + + * catch up to trunk cloud-init (rev 385). + * attempt to install packages on failed apt-get update (LP: #728167) + * enabled timezone and mcollective cloud-config plugins + + -- Scott Moser Fri, 04 Mar 2011 21:17:21 -0500 + +cloud-init (0.6.1-0ubuntu2) natty; urgency=low + + * grub-legacy-ec2: Use dpkg-query --control-path instead of hard-coding a + path to debconf templates file, for compatibility with multiarch. + + -- Scott Moser Tue, 01 Mar 2011 23:23:55 -0500 + +cloud-init (0.6.1-0ubuntu1) natty; urgency=low + + * New upstream release. + * fix for puppet configuration options (LP: #709946) [Ryan Lane] + * fix pickling of DataSource, which broke seeding. + * turn resize_rootfs default to True + * avoid mounts in DataSourceOVF if 'read' on device fails + 'mount /dev/sr0' for an empty virtual cdrom device was taking 18 seconds + * add 'manual_cache_clean' option to select manual cleaning of + the /var/lib/cloud/instance/ link, for a data source that might + not be present on every boot + * make DataSourceEc2 retries and timeout configurable + * add 'bootcmd' like 'runcmd' to cloud-config syntax for running things early + * move from '#opt_include' in config file format to conf_d. + now local config files should live in /etc/cloud/cloud.cfg.d/ + * move /etc/cloud/distro.cfg to /etc/cloud/cloud.cfg.d/90_dpkg.cfg + * allow /etc/hosts to be written from hosts.tmpl. which allows + getting local-hostname into /etc/hosts (LP: #720440) + * better handle startup if there is no eth0 (LP: #714807) + * update rather than append in puppet config [Marc Cluet] + * add cloud-config for mcollective [Marc Cluet] + + -- Scott Moser Sat, 19 Feb 2011 01:16:10 -0500 + +cloud-init (0.6.0-0ubuntu4) natty; urgency=low + + * fix running of user scripts (LP: #711480) (cherry pick 344) + * fix 2 lintian warnings + + -- Scott Moser Tue, 01 Feb 2011 16:15:30 -0500 + +cloud-init (0.6.0-0ubuntu3) natty; urgency=low + + * make a better attempt at deciding if DataSourceEc2 should be used + on first install or upgrade + * fix behavior if def_log_file is empty in cloud-config (cherry pick 333) + * improve comment strings in rsyslog config (cherry pick 334) + * do not package cloud-init query (cherry pick 335) + * add previous-instance-id and previous-datasource to cloud/data + and cloud/instance/datasource files (cherry pick 337) + * allow setting of passwords and enabling/disabling ssh password auth + via cloud-config (cherry pick 338) + + -- Scott Moser Mon, 31 Jan 2011 12:48:39 -0500 + +cloud-init (0.6.0-0ubuntu2) natty; urgency=low + + * add a debian/README.source file + * fix bug in fixing permission on /var/log/cloud-init.log (cherry pick) + * remove dependency on update-motd as updates-check was removed + * fix failure on cloud-init package purge + * add configuration of DataSources via debconf. Default to not searching + Ec2. (LP: #635188) + * fix naming of pre-processed (now user-data.txt.i) (cherry pick) + * upgrade existing content in /var/lib/cloud to 0.6.x format + + -- Scott Moser Thu, 27 Jan 2011 16:32:44 -0500 + +cloud-init (0.6.0-0ubuntu1) natty; urgency=low + + * New upstream release. + * fix permissions on cloud-init.log so syslog can write to it (LP: ##704509) + * rework of /var/lib/cloud layout + * remove updates-check (LP: #653220) + * support resizing root partition on first boot (enabled by default) + * added cloud-config options for setting hostname, phone_home + * indicate "all the way up" with message to console and file creation + in /var/lib/cloud/instance/ (LP: #653271) + * write ssh keys to console late in boot to ensure they're in console buffer + * add support for redirecting output of cloud-init, cloud-config, + cloud-final via the config file, or user data config file + * add support for posting data about the instance to a url (phone_home) + * add minimal OVF transport (iso) support + * make DataSources that are attempted dynamic and configurable from + config. config option 'cloud_type' replaced by 'datasource_list' + * add 'timezone' option to cloud-config (LP: #645458) + * Added an additional archive format, that can be used for multi-part + input to cloud-init. This may be more user friendly then mime-multipart + (LP: #641504) + * add support for reading Rightscale style user data (LP: #668400) + * make the message on 'disable_root' more clear (LP: #672417) + * do not require public key if private is given in ssh cloud-config + (LP: #648905) + + -- Scott Moser Wed, 26 Jan 2011 17:28:36 -0500 + +cloud-init (0.5.15-0ubuntu4) natty; urgency=low + + * Rebuild with python 2.7 as the python default. + + -- Matthias Klose Wed, 08 Dec 2010 15:01:36 +0000 + +cloud-init (0.5.15-0ubuntu3) maverick; urgency=low + + * do not use ec2 ubuntu archive if instance is VPC (LP: #615545) + + -- Scott Moser Thu, 16 Sep 2010 04:28:55 -0400 + +cloud-init (0.5.15-0ubuntu2) maverick; urgency=low + + * grub-legacy-ec2: boot with console=hvc0 (LP: #606373) + + -- Scott Moser Wed, 15 Sep 2010 16:41:48 -0400 + +cloud-init (0.5.15-0ubuntu1) maverick; urgency=low + + * New upstream release. + * fix /etc/fstab cloudconfig entries for t1.micro and + change default fstab values for ephemeral0 to nobootwait (LP: #634102) + * grub-legacy-ec2: do not write chainload for grub2 to menu.lst + (LP: #627451) + * seed grub-pc correctly so update-grub runs on ec2 or uec(LP: #623609) + + -- Scott Moser Sun, 12 Sep 2010 15:23:39 -0400 + +cloud-init (0.5.14-0ubuntu5) maverick; urgency=low + + * add missing imports for cc_puppet (LP: #632744) + * append to apt_sources files rather than truncating (LP: #627597) + * get double commented lines into sources.list (LP: #627439) + + -- Scott Moser Wed, 08 Sep 2010 10:31:58 -0400 + +cloud-init (0.5.14-0ubuntu4) maverick; urgency=low + + * add commented out entries for partner, backports, and multiverse + (LP: #620572) + + -- Scott Moser Thu, 26 Aug 2010 16:44:48 -0400 + +cloud-init (0.5.14-0ubuntu3) maverick; urgency=low + + * fix syntax error in cloudinit/util.py (failed installation) + + -- Scott Moser Tue, 17 Aug 2010 22:22:06 -0400 + +cloud-init (0.5.14-0ubuntu2) maverick; urgency=low + + * fix bug preventing 'seedfrom' from working (LP:617400) + + -- Scott Moser Tue, 17 Aug 2010 15:49:13 -0400 + +cloud-init (0.5.14-0ubuntu1) maverick; urgency=low + + * New upstream release. + - support for reading metadata and userdata from filesystem + - support for boot without metadata at all + + -- Scott Moser Thu, 12 Aug 2010 14:45:28 -0400 + +cloud-init (0.5.13-0ubuntu3) maverick; urgency=low + + * grub-legacy-ec2: fix 'apt-get --reinstall' and dpkg-divert (LP: #611812) + * enable -virtual kernels as "xen" kernels (pv_ops now functional) + * fix bad syntax in cloud-init-run-module.py + + -- Scott Moser Mon, 02 Aug 2010 16:26:48 -0400 + +cloud-init (0.5.13-0ubuntu2) maverick; urgency=low + + * debian/control: drop ssh-import as a recommends, as this has been + subsumed by openssh-server + + -- Dustin Kirkland Sat, 24 Jul 2010 21:02:40 +0200 + +cloud-init (0.5.13-0ubuntu1) maverick; urgency=low + + * New upstream release. + * invoke dpkg with --force-confold (LP: #607642) + + -- Scott Moser Wed, 21 Jul 2010 11:58:53 -0400 + +cloud-init (0.5.12-0ubuntu8) maverick; urgency=low + + * update-grub-legacy-ec2: + - add code to stop use of 'uuid' grub syntax + - change fallback grub device from (hd0,0) to (hd0) + - change timeout in menu.lst to 0 + * grub-legacy-ec2: add grub-set-default functionality (LP: #605961) + + -- Scott Moser Thu, 15 Jul 2010 13:07:01 -0400 + +cloud-init (0.5.12-0ubuntu7) maverick; urgency=low + + * update-grub-legacy-ec2: + - force setting of indomU so output of build process has + a menu.lst with -ec2 kernels listed. + - remove 'quite splash' from kernel options + - make sure grub_root_device is set to hd0 in image build + + -- Scott Moser Tue, 13 Jul 2010 16:33:51 -0400 + +cloud-init (0.5.12-0ubuntu6) maverick; urgency=low + + * fix installation error + * add quilt-setup rule for package development + + -- Scott Moser Tue, 13 Jul 2010 12:04:21 -0400 + +cloud-init (0.5.12-0ubuntu5) maverick; urgency=low + + * sync with upstream r226. + * fix bug where nfs/network mounts could not be specified (LP: #603329) + * manage hostname setting better (LP: #596993) + * add legacy-grub-ec2 package. + + -- Scott Moser Thu, 08 Jul 2010 22:24:59 -0400 + +cloud-init (0.5.12-0ubuntu4) maverick; urgency=low + + * handle hostname managing better with ebs root. (LP: #596993) + + -- Scott Moser Wed, 07 Jul 2010 11:54:10 -0400 + +cloud-init (0.5.12-0ubuntu3) maverick; urgency=low + + * fix cloud-boothook input type (LP: #600799) + * sync with upstream. + + -- Scott Moser Thu, 01 Jul 2010 21:19:13 -0400 + +cloud-init (0.5.12-0ubuntu2) maverick; urgency=low + + * fix cloud config 'apt-update-upgrade' failure due to missing import + + -- Scott Moser Mon, 21 Jun 2010 15:08:32 -0400 + +cloud-init (0.5.12-0ubuntu1) maverick; urgency=low + + * New upstream release. + * fix cloud-init-run-module to allow 'always' (LP: #568139) + * add support for setting debconf selections + * add cloud-config support for debconf selections (LP: #582667), + byobu enablement, and ssh-import-lp-id + + -- Scott Moser Fri, 18 Jun 2010 15:48:14 -0400 + +cloud-init (0.5.11-0ubuntu2) maverick; urgency=low + + * handle renaming sem/markers for config items to avoid + running per-instance again after package upgrade + * pull 'config-' prefix on sem/ items for cloud-config modules from + 0.5.12 + + -- Scott Moser Fri, 18 Jun 2010 12:52:10 -0400 + +cloud-init (0.5.11-0ubuntu1) maverick; urgency=low + + * New upstream release. + * remove ec2-get-info. It is replaced by cloudutils ec2metadata + * use python logging + * reduce number of upstart jobs + * add "boothook" user data type + * Switch to dpkg-source 3.0 (quilt) format + + -- Scott Moser Fri, 18 Jun 2010 01:04:58 -0400 + +cloud-init (0.5.10-0ubuntu1) lucid; urgency=low + + * New upstream release. + * ec2-get-info: fix for python traceback + * ephemeral mount will show up in /etc/mtab or df on first boot + LP: #527825 + + -- Scott Moser Fri, 26 Mar 2010 00:57:28 -0400 + +cloud-init (0.5.9-0ubuntu1) lucid; urgency=low + + * New upstream release. + * rename apt list files. 'ubuntu-bug' now works without update (LP: #513060) + * replace 'cloudconfig' entries in fstab rather than appending (LP: #524562) + * fix to fstab writing on ebs-root instances + + -- Scott Moser Mon, 08 Mar 2010 13:07:02 -0500 + +cloud-init (0.5.8-0ubuntu1) lucid; urgency=low + + * New upstream release. + * cache data from metadata service, LP: #527364 + * fix format of cron entry in cron.d/cloudinit-updates + * package egg-info file + + -- Scott Moser Tue, 02 Mar 2010 15:48:04 -0500 + +cloud-init (0.5.7-0ubuntu4) lucid; urgency=low + + * fix empty package previous package was emptpy for cloud-init + due to adding the ec2-init package + + -- Scott Moser Fri, 26 Feb 2010 17:06:05 -0500 + +cloud-init (0.5.7-0ubuntu3) lucid; urgency=low + + * debian/control: + - recommend ssh-import, such that the ssh-import-lp-id + utility is available in UEC images for convenient importing of + ssh public keys stored in Launchpad, LP: #524101 + - build a transitional ec2-init package to handle the rename + gracefully on upgrades, LP: #527187 + + -- Dustin Kirkland Thu, 25 Feb 2010 16:22:10 -0600 + +cloud-init (0.5.7-0ubuntu2) lucid; urgency=low + + * fix packaging but that put the message-of-the-day hook file + into a subdir of etc/update-motd.d, remove old file (LP: #524999) + + -- Scott Moser Fri, 19 Feb 2010 21:02:10 -0500 + +cloud-init (0.5.7-0ubuntu1) lucid; urgency=low + + * New upstream release. + * run cloud-init early in boot process (LP: #504883, #524516) + + -- Scott Moser Fri, 19 Feb 2010 18:27:45 -0500 + +cloud-init (0.5.6-0ubuntu1) lucid; urgency=low + + * New upstream release. + * supports 'runcmd' in cloud-config + * enable the update check code (LP: #524258) + * fix retry_url in boto_utils.py when metadata service not around + (LP: #523832) + * run cloud-config-puppet.conf later (LP: #523625) + [ Scott Moser 0.5.5 ] + * New upstream release, supports checking for updates + + -- Scott Moser Fri, 19 Feb 2010 03:13:22 -0500 + +cloud-init (0.5.4-0ubuntu1) lucid; urgency=low + + * New upstream release. + * fix broken user-data scripts + * merge mathiaz work for cloud-config-puppet + * fix bug causing apt update to fail + * rename EC2Init class to CloudInit + * only set hostname once per instance. (LP: #514492) + + -- Scott Moser Wed, 17 Feb 2010 09:40:30 -0500 + +cloud-init (0.5.3-0ubuntu2) lucid; urgency=low + + * divert ureadahead.conf in postinst (LP: #499520) + * lintian cleanups + + -- Scott Moser Fri, 05 Feb 2010 15:48:21 -0500 + +cloud-init (0.5.3-0ubuntu1) lucid; urgency=low + + * Rename ec2-init to cloud-init. New upstream release. + * set hostname to ip-u.x.y.z if local-hostname provides a ip addr + (LP: #475354) + + -- Scott Moser Thu, 04 Feb 2010 03:00:05 -0500 + +ec2-init (0.5.2-0ubuntu1) lucid; urgency=low + + * new upstream release + + -- Scott Moser Fri, 29 Jan 2010 13:30:52 -0500 + +ec2-init (0.5.1-0ubuntu1) lucid; urgency=low + + * new upstream release + + -- Scott Moser Fri, 22 Jan 2010 16:19:30 -0500 + +ec2-init (0.5.0-0ubuntu4) lucid; urgency=low + + * add an upstart job to get ssh keys regenerated and written + to console (LP: #506599, LP: #507070) + + -- Scott Moser Thu, 14 Jan 2010 13:10:55 -0500 + +ec2-init (0.5.0-0ubuntu3) lucid; urgency=low + + * work around difference in uec/ec2 metadata service (LP:506332) + + -- Scott Moser Tue, 12 Jan 2010 11:33:11 -0500 + +ec2-init (0.5.0-0ubuntu2) lucid; urgency=low + + * pull changes from devel branch to get functional on ec2 + + -- Scott Moser Mon, 11 Jan 2010 12:03:45 -0500 + +ec2-init (0.5.0-0ubuntu2) lucid; urgency=low + + * new upstream release + + -- Scott Moser Thu, 07 Jan 2010 22:00:38 -0500 + +ec2-init (0.4.999-0ubuntu8) lucid; urgency=low + + * fix mirror selection for us-west-1 (LP: #494185) + + -- Scott Moser Fri, 11 Dec 2009 15:12:19 -0500 + +ec2-init (0.4.999-0ubuntu7) karmic; urgency=low + + * work around differences in eucalyptus ephemeral mounts (LP: #458850) + * get 'ec2:' prefix on ssh public key fingerprint (LP: #458576) + + -- Scott Moser Mon, 26 Oct 2009 16:18:06 -0400 + +ec2-init (0.4.999-0ubuntu6) karmic; urgency=low + + * make sources.list components for 'karmic-security' the same as + 'karmic' and 'karmic-updates' (main, restricted) (LP: #457866) + + -- Scott Moser Thu, 22 Oct 2009 08:55:58 -0400 + +ec2-init (0.4.999-0ubuntu5) karmic; urgency=low + + * write regenerate_ssh_host_keys output directly to /dev/console + to ensure that it gets there. (LP: #451881) + + -- Scott Moser Wed, 21 Oct 2009 17:23:38 -0400 + +ec2-init (0.4.999-0ubuntu4) karmic; urgency=low + + * set locale to en_US.UTF-8 if get_location_from_availability_zone + doesn't have a match (LP: #407949) + + -- Scott Moser Tue, 20 Oct 2009 09:57:49 -0400 + +ec2-init (0.4.999-0ubuntu3) karmic; urgency=low + + * split running of user-data out of ec2-init into ec2-init-user-data + run this at S99. (LP : #431255) + + -- Scott Moser Fri, 25 Sep 2009 14:17:17 -0400 + +ec2-init (0.4.999-0ubuntu2) karmic; urgency=low + + * remove rightscale-init from package (see LP: #434181, LP: #434693) + * fix lintian warning, specify path to GPL-3 + * replace multiple '| logger' in regenerate_ssh_host_keys single one + * add ec2-is-compat-env, and disable init script by default. it can be + enabled by setting 'compat=1' in /etc/ec2-init/is-compat-env + + -- Scott Moser Thu, 24 Sep 2009 16:32:42 -0400 + +ec2-init (0.4.999-0ubuntu1) karmic; urgency=low + + * New upstream release + + -- Soren Hansen Wed, 26 Aug 2009 01:23:52 +0200 + +ec2-init (0.4.99-0ubuntu3) karmic; urgency=low + + * Also update /etc/default/locale when setting the locale based on + locality. + + -- Soren Hansen Tue, 11 Aug 2009 21:49:33 +0200 + +ec2-init (0.4.99-0ubuntu2) karmic; urgency=low + + * Consolidate build-dependencies a bit. + * Sync default configuration with code. + * Create /var/lib/ec2 in package. + * Make ec2-get-info more robust in testing environments. + * Handle missing public keys more gracefully. + * Set proper ownership of user's authorized_keys. + + -- Soren Hansen Tue, 11 Aug 2009 09:54:16 +0200 + +ec2-init (0.4.99-0ubuntu1) karmic; urgency=low + + * Massive rewrite. (LP: #407871, #407919, #308530, #407949, #407950, #407892 + and probably many others) + * First Ubuntu version with the upstream tarball split out. + * Switch to arch: all. There is no arch specific code here. + + -- Soren Hansen Tue, 11 Aug 2009 08:33:33 +0200 + +ec2-init (0.3.4ubuntu9) karmic; urgency=low + + * Really include the action id in the semaphore filename. + + -- Soren Hansen Sat, 11 Jul 2009 09:50:31 +0200 + +ec2-init (0.3.4ubuntu8) karmic; urgency=low + + * Add Vcs-Bzr header to debian/control. + * Include the action id in the semaphore filename. + + -- Soren Hansen Sat, 11 Jul 2009 02:35:43 +0200 + +ec2-init (0.3.4ubuntu7) karmic; urgency=low + + * Re-add accidentally removed locale template. + * Fix indentation in init script. + + -- Soren Hansen Fri, 10 Jul 2009 23:47:27 +0200 + +ec2-init (0.3.4ubuntu6) karmic; urgency=low + + * Replace calls to ec2-get-data with calls to ec2-get-info. + * Make this package arch: all. + + -- Soren Hansen Fri, 10 Jul 2009 23:16:35 +0200 + +ec2-init (0.3.4ubuntu5) karmic; urgency=low + + * debian/init: Move instance reboot detection out of python + scripts and move them to the init script. + * debian/ec2-set-defaults.py: + - Point to the right location for locate (LP: #387611) + - Default to UTC (LP: #341060) + * debian/ec2-set-apt-sources.py: If you cant contact EC2 then use the + regular archive (LP: #387027) + * debian/ec2-setup-hostname.py: Use the local hostname in /etc/hosts + and dont change once rebooted. (LP: #352745) + + -- Chuck Short Wed, 08 Jul 2009 09:48:49 -0400 + +ec2-init (0.3.4ubuntu4) karmic; urgency=low + + * debian/init: Run update-motd regardless whether its a firstboot or not. + * debian/init: Fix comments (LP: #373057) + * debian/control: Add update-motd as a depends. + * ec2-set-defaults.py: Wait for network to become available. (LP: #308530) + + -- Chuck Short Thu, 28 May 2009 05:04:31 -0400 + +ec2-init (0.3.4ubuntu3) karmic; urgency=low + + * debian/init: Move init script to run before ssh and regenerate the ssh + host kes in the init script rather than /etc/rc.local (LP: #370628) + * ec2-set-apt-sources.py: + - Move sources.list to /var/ec2 so it doesnt get removed after user + reboots. + * ec2-set-defaults.py: + - Move locale to /var/ec2/ so it doesnt get remove after user reboots. + * ec2-set-hostname.py + - Create an /etc/hostname as well. + + -- Chuck Short Thu, 14 May 2009 11:11:49 -0400 + +ec2-init (0.3.4ubuntu2) karmic; urgency=low + + * Really change the locale when setting up an instance. (LP: #341066) + * Run ec2-run-user-data script last. (LP: #373055) + * Minor comment tweaks. (LP: #373057) + + -- Chuck Short Wed, 13 May 2009 13:41:35 -0400 + +ec2-init (0.3.4ubuntu1) karmic; urgency=low + + * Add more smarts to ec2 instance bring up. (LP: #371936) + + -- Chuck Short Tue, 05 May 2009 08:59:54 -0400 + +ec2-init (0.3.3ubuntu12) jaunty; urgency=low + + * ec2-run-user-data.py: Fix error. + + -- Chuck Short Tue, 07 Apr 2009 08:14:07 -0400 + +ec2-init (0.3.3ubuntu11) jaunty; urgency=low + + * debian/control: + - Add python-cheetah and python-apt as a dependency. + * debian/ec2-config.cfg: + - Remove distro due to the change in ec2-set-apt-sources.py + * debian/inistall + - Install the templates in the right place. + * ec2-set-apt-sources.py: + - Use python-apt to update the sources.list. + + -- Chuck Short Wed, 01 Apr 2009 13:58:43 -0400 + +ec2-init (0.3.3ubuntu10) jaunty; urgency=low + + * ec2-set-hostname.py: + - Use template for /etc/hosts creation. + - Dont use public_hostname in /etc/hosts. (LP: #352745) + + -- Chuck Short Wed, 01 Apr 2009 08:48:05 -0400 + +ec2-init (0.3.3ubuntu9) jaunty; urgency=low + + * ec2-set-apt-sources.py: + - Use a template to generate the sources.list and generate it based on the lsb_release. + + -- Chuck Short Tue, 31 Mar 2009 15:15:55 -0400 + +ec2-init (0.3.3ubuntu8) jaunty; urgency=low + + * ec2-set-apt-sources.py: + - Add the ubuntu-on-ec2 ppa. + + -- Chuck Short Tue, 31 Mar 2009 09:37:13 -0400 + +ec2-init (0.3.3ubuntu7) jaunty; urgency=low + + * debian/rules: Fix typo. + + -- Chuck Short Sun, 22 Mar 2009 17:14:16 -0400 + +ec2-init (0.3.3ubuntu6) jaunty; urgency=low + + * Set the configuration file to jaunty. + * ec2-fetch-credentials: Fix typo. + * ec2-set-defaults.py: + - Remove timezone change when booting the instance. + - Redirect output to /dev/null. + * ec2-set-apt-sources.py: + - Run apt-get update after the /etc/apt/sources.list and + redirect the output to /dev/null. + * rightscale-init: Updated rightscale-init + + -- Chuck Short Thu, 19 Mar 2009 20:52:59 -0400 + +ec2-init (0.3.3ubuntu5) jaunty; urgency=low + + * debian/ec2-config.cfg: + - Add disable root option. + * debian/ec2-init.rightscale-init.init: + - Add rightscale detection script. + * ec2-get-info.py: + - Display the information about an AMI instance. + + -- Chuck Short Mon, 16 Mar 2009 08:54:49 -0400 + +ec2-init (0.3.3ubuntu4) jaunty; urgency=low + + * ec2-fetch-credentials.py: + - Allow user to choose which user they wish to configure for. + - Allow user to disable root user if they wish to. + * ec2-set-defaults.py: + - Set default timezone to UTC. + - Set locale depending on zone. + * debian/init: + - Removed nash plugin. + - Add ec2-set-defaults. + + -- Chuck Short Wed, 04 Mar 2009 08:33:01 -0500 + +ec2-init (0.3.3ubuntu3~intrepid4) intrepid; urgency=low + + * set distro to intrepid. + + -- Chuck Short Thu, 26 Feb 2009 10:28:06 -0500 + +ec2-init (0.3.3ubuntu3) jaunty; urgency=low + + * debian/ec2-init: Log results of ec2-run-user-data to syslog. + * ec2-run-user-data.py :Dont leave files around and log the + output to syslog. + + -- Chuck Short Thu, 26 Feb 2009 10:24:35 -0500 + +ec2-init (0.3.3ubuntu2) jaunty; urgency=low + + * ec2-set-apt-sources.py: + - Use the ec2 mirrors. (LP: #317065, #333897) + - Update the /etc/apt/sources.list (LP: #333904) + * ec2-fetch-credentials.py: + - Better error checking (LP: #325067) + + -- Chuck Short Tue, 24 Feb 2009 14:02:37 -0500 + +ec2-init (0.3.3ubuntu1) jaunty; urgency=low + + * debian/init: Fix init script. + + -- Chuck Short Fri, 20 Feb 2009 09:22:54 -0500 + +ec2-init (0.3.3) jaunty; urgency=low + + * ec2-set-apt-sources.py + - Determine the zone that the user is in and generate + a /etc/apt/sources.list.d/ based on that. + * debian/init: + - Check to see if there is an /var/run/ec2 and create + it if it doesnt exist. + - Start ec2-set-apt-sources at first bootup. + * debian/rules: + - Install ec2-set-apt-sources. + * debian/control: + - Add python-configobj as a dependency. + * debian/{install,dirs} + - Create an /etc/ec2-init to read the configuration file and install it. + + -- Chuck Short Mon, 09 Feb 2009 10:35:56 -0500 + +ec2-init (0.3.2) jaunty; urgency=low + + * debian/init: + - Remove already ran detection + - Log the running of ec2-run-user-data to /var/log/ec2-user-data.log + * ec2-set-hostname.py: + - set hostname to the Ec2 local-hostname + - Update the /etc/hosts to change the ubuntu hostname to the + public hostname. + * ec2-fetch-credentials: + - Copy the ssh keys to the ubuntu user. + - Setup authorized keys for root to tell the user to login as the + ubuntu user when they try to connect. + * ec2-run-user-data: + - Create an .already-ran file to check to see if ec2-run-user-data + already ran. + - Save the ec2-run-user-data script in /var/ec2. + + -- Chuck Short Wed, 04 Feb 2009 09:32:08 -0500 + +ec2-init (0.3.1) jaunty; urgency=low + + * debian/dir: Install /var/ec2 to save user-data scripts. + * debian/rules: Start ec2-init after ssh. + * ec2-run-user-data.py: Save run-user-data script with ami-id. + + -- Chuck Short Mon, 26 Jan 2009 10:40:52 -0500 + +ec2-init (0.3) jaunty; urgency=low + + * ec2-run-user-data: Fix python error when writing a file to the disk. + + -- Chuck Short Thu, 15 Jan 2009 11:49:08 -0500 + +ec2-init (0.2) jaunty; urgency=low + + * debian/init: Run fetch-credentials before anything else. + (LP: #308533) + * Add ec2-set-hostname.py: Queries ec2 metdada for public-hostname + and then sets it (LP: #316201) + + -- Chuck Short Tue, 13 Jan 2009 15:20:21 -0500 + +ec2-init (0.1) intrepid; urgency=low + + * Initial release (LP: #269434). + + -- Soren Hansen Fri, 12 Sep 2008 15:30:32 +0200 diff --git a/debian/cherry-pick b/debian/cherry-pick new file mode 100755 index 00000000000..0a5cea39b34 --- /dev/null +++ b/debian/cherry-pick @@ -0,0 +1,197 @@ +#!/bin/bash + +VERBOSITY=0 +TEMP_D="" +CR=$'\n' + +error() { echo "$@" 1>&2; } +fail() { [ $# -eq 0 ] || error "$@"; exit 1; } + +Usage() { + cat <> + + Cherry pick a patch into debian/patches. + Useful to grab an upstream commit to the current packaging branch. + + options: + -h | --help show help +EOF +} + +bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; } +cleanup() { + [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}" +} + +debug() { + local level=${1}; shift; + [ "${level}" -gt "${VERBOSITY}" ] && return + error "${@}" +} + +shorten() { + local name="$1" len="70" + while [ "${#name}" -gt "$len" ]; do + name="${name%-*}" + done + _RET="$name" +} + +print_commit() { + local subject="$1" author="$2" bugs="$3" aname="" + aname=${author% <*} + echo "$subject${bugs:+ (LP: ${bugs})}" +} + +print_bugs() { + local subject="$1" author="$2" bugs="$3" aname="" + echo "$bugs" +} + +git_log_to_dch() { + # call printer with subject, author and bugs as extracted + # from either git format-patch output or git show output. + local line="" commit="" lcommit="" bugs="" + local printer="${1:-print_commit}" + while :; do + read line || break + case "$line" in + commit\ *|From\ *) + if [ -n "$commit" ]; then + "$printer" "$subject" "$author" "$bugs" + fi + commit=${line#* } + commit=${commit%% *} + bugs="" + author="" + subject="" + ;; + Author:\ *|From:\ *) author="${line#*: }";; + LP:*) bugs="${bugs:+${bugs}, }${line#*: }";; + "") [ -z "$subject" ] && read subject;; + Subject:\ *) + subject="${line#Subject: }" + subject="${subject#\[PATCH\] }" + ;; + esac + done + if [ -n "$commit" ]; then + "$printer" "$subject" "$author" "$bugs" + fi +} + +main() { + local short_opts="ho:v" + local long_opts="help,verbose" + local getopt_out="" + getopt_out=$(getopt --name "${0##*/}" \ + --options "${short_opts}" --long "${long_opts}" -- "$@") && + eval set -- "${getopt_out}" || + { bad_Usage; return; } + + local cur="" next="" + + while [ $# -ne 0 ]; do + cur="$1"; next="$2"; + case "$cur" in + -h|--help) Usage ; exit 0;; + -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));; + --) shift; break;; + esac + shift; + done + + [ -n "$TEMP_D" ] || + TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") || + { error "failed to make tempdir"; return 1; } + trap cleanup EXIT + + [ $# -gt 0 ] || { bad_Usage "must provide commit-ish"; return; } + + local r="" commit_in="$1" chash="" shash="" sname="" fname="" cur_br="" + cur_br=$(git rev-parse --abbrev-ref HEAD) || + { error "failed to get current branch"; return 1; } + chash=$(git show --quiet "--pretty=format:%H" "${commit_in}") || + { error "failed git show $commit_in"; return 1; } + + if git merge-base --is-ancestor "$chash" HEAD; then + error "current branch '$cur_br' already contains $commit_in ($chash)" + return 1 + fi + + out=$(git show --quiet "--pretty=format:%h %f" "$chash") || + { error "failed git show $chash"; return 1; } + + shash=${out% *} + sname=${out#* } + longname="cpick-$shash-$sname" + shorten "$longname" + fname="$_RET" + + [ -d debian/patches ] || mkdir -p debian/patches || + { error "failed to make debian/patches"; return 1; } + + local series="debian/patches/series" fpath="debian/patches/$fname" + if [ -e "$series" ] && out=$(grep -- "-${shash}-" "$series"); then + error "$chash already exists in $series" + error " $out" + return 1 + fi + + if [ -e "$series" ]; then + if out=$(quilt applied 2>&1); then + error "there are quilt patches applied!" + error "$out" + return 1 + fi + fi + + git format-patch --stdout -1 "$chash" > "$fpath" || + { error "failed git format-patch -1 $chash > $fpath"; return 1; } + + echo "$fname" >> "$series" || + { error "failed to write to $series"; return 1; } + + quilt push "$fname" || + { error "patches do not cleanly apply"; return 1; } + quilt refresh && quilt pop -a || + { error "failed to refresh or pop quilt"; return 1; } + + local message="" + message=$(git_log_to_dch < "$fpath") || + { error "failed getting log entry from $fpath"; return 1; } + dch -i "cherry-pick $shash: $message" + + dch -e || { + r=$?; + error "dch -e exited $r"; + return $r; + } + + local commit_files="" + commit_files=( "$series" "$fpath" ) + git diff HEAD "${commit_files[@]}" + + echo -n "Commit this change? (Y/n): " + read answer || fail "failed to read answer" + case "$answer" in + n|[Nn][oO]) exit 1;; + esac + + bugs=$(git_log_to_dch print_bugs < "$fpath") + msg="cherry pick $shash${bugs:+${CR}${CR}LP: ${bugs}}" + git add "$series" "$fpath" || + { error "failed to git add $series $fpath"; return 1; } + + git commit -m "$msg" "${commit_files[@]}" || + fail "failed to commit '$msg'" + + git commit -m "update changelog" debian/changelog || + fail "failed to commit update to debian changelog." + + return 0 +} + +main "$@" +# vi: ts=4 expandtab diff --git a/debian/clean b/debian/clean new file mode 100644 index 00000000000..de33a5f4410 --- /dev/null +++ b/debian/clean @@ -0,0 +1,2 @@ +.pytest_cache/ +cloud_init.egg-info/ diff --git a/debian/cloud-init-base.config b/debian/cloud-init-base.config new file mode 100644 index 00000000000..43c658f23ab --- /dev/null +++ b/debian/cloud-init-base.config @@ -0,0 +1,99 @@ +#!/bin/sh + +set -e + +. /usr/share/debconf/confmodule + +hasEc2Md() { + local d=/var/lib/cloud i=/var/lib/cloud/instance/ f="" + local flist="${i}/datasource ${i}/obj.pkl ${d}/cache/obj.pkl" + # search current instance data source information + for f in ${flist}; do + [ -f "${f}" ] || continue + grep -q DataSourceEc2 "${f}" 2>/dev/null && return 0 + done + + + # there was no data above found that would have been indicated + # by a upgrade. lets just see if we can't find the metadata + # service. if wget is not present (it is not 'essential') + # then we will quietly return 1 + local url="http://169.254.169.254/2009-04-04/meta-data/instance-id" + i="" + if command -v wget >/dev/null 2>&1; then + local tout="--connect-timeout 5 --read-timeout 5 --tries 1" + i=$(wget "${url}" ${tout} -O - 2>/dev/null) || : + elif command -v curl >/dev/null 2>&1; then + i=$(curl "${url}" --max-time 1) || : + fi + # if instance-id starts with 'i-' then in all likelyhood its good + [ "${i#i-}" != "${i}" ] && return 0 + return 1 +} + +get_yaml_list() { + # get_yaml_list(file, key, def): return a comma delimited list with the value + # for the yaml array defined in 'key' from 'file'. if not found , return 'def' + # only really supports 'key: [ en1, en2 ]' or 'key: [en1, en2]' formats. + local file="$1" key="$2" default="$3" + [ -f "$file" ] || return 1 + # strip all whitespace, delete lines not matching key:, + # strip key: and [] and replace ',' with ', ' + RET="$(sed -e "s/\s//g" -e "/^$key:/"'!'d \ + -e "s/$key:\[//" -e "s/]//" -e "s/,/, /g" "$file")" + [ -n "$RET" ] || RET="$default" +} + +# Migrate a debconf value from cloud-init/* to cloud-init-base/* +# +# Arguments: +# Name of the debconf question +migrate_debconf_to_cloud_init_base() { + local old="cloud-init/$1" new="cloud-init-base/$1" + if db_get "$new"; then + # cloud-init-base/* win against cloud-init/* values + return 0 + fi + if db_get "$old"; then + echo "Migrating debconf value: ${old} -> ${new}" 1>&2 || : + db_set "$new" "$RET" + db_unregister "$old" || : + fi +} + +# old_dpkg_cfg is very old file that is no longer read by cloud-init. +# it gets re-named to cloud.cfg.d/90_dpkg.cfg in the preinst. +dpkg_cfg="/etc/cloud/cloud.cfg.d/90_dpkg.cfg" +old_dpkg_cfg="/etc/cloud/distro.cfg" +if [ -f "${old_dpkg_cfg}" -a ! -f "$dpkg_cfg" ]; then + dpkg_cfg="${old_dpkg_cfg}" + echo "WARN: reading value from ${old_dpkg_cfg}" 1>&2 +fi + +migrate_debconf_to_cloud_init_base datasources + +if [ -f "$dpkg_cfg" ]; then + if get_yaml_list "$dpkg_cfg" datasource_list NOTFOUND && + val="$RET" && [ "$val" != "NOTFOUND" ]; then + db_set cloud-init-base/datasources "$val" + else + echo "WARN: failed to read datasource_list from $dpkg_cfg" 1>&2 + fi +elif { db_fget cloud-init-base/datasources seen || : ; } && + [ "${RET}" = "false" ]; then + # this is the first time this we've run (installation or re-install after + # purge). try to determine if the Ec2 datasource is there. + # if it is, and Ec2 was not in the default list, then add it. + db_get cloud-init-base/datasources + def="${RET}" + case " ${def}," in + *\ Ec2,*) :;; + *) hasEc2Md && + db_set cloud-init-base/datasources "${def:+${def}, }Ec2";; + esac +fi + +db_input low cloud-init-base/datasources || true +db_go + +exit 0 diff --git a/debian/cloud-init-base.lintian-overrides b/debian/cloud-init-base.lintian-overrides new file mode 100644 index 00000000000..e21a63cdd67 --- /dev/null +++ b/debian/cloud-init-base.lintian-overrides @@ -0,0 +1,6 @@ +# cloud-init.target should not be considered as unusual +cloud-init-base binary: systemd-service-file-refers-to-unusual-wantedby-target cloud-init.target [usr/lib/systemd/system/cloud-config.service] +cloud-init-base binary: systemd-service-file-refers-to-unusual-wantedby-target cloud-init.target [usr/lib/systemd/system/cloud-final.service] +cloud-init-base binary: systemd-service-file-refers-to-unusual-wantedby-target cloud-init.target [usr/lib/systemd/system/cloud-init-local.service] +cloud-init-base binary: systemd-service-file-refers-to-unusual-wantedby-target cloud-init.target [usr/lib/systemd/system/cloud-init-network.service] +cloud-init-base binary: systemd-service-file-refers-to-unusual-wantedby-target cloud-init.target [usr/lib/systemd/system/cloud-init-main.service] diff --git a/debian/cloud-init-base.logrotate b/debian/cloud-init-base.logrotate new file mode 100644 index 00000000000..a2835df061b --- /dev/null +++ b/debian/cloud-init-base.logrotate @@ -0,0 +1,11 @@ +/var/log/cloud-init*.log +{ + su root root + missingok + nocreate + notifempty + rotate 6 + compress + delaycompress + size 1M +} diff --git a/debian/cloud-init-base.maintscript b/debian/cloud-init-base.maintscript new file mode 100644 index 00000000000..9d331268321 --- /dev/null +++ b/debian/cloud-init-base.maintscript @@ -0,0 +1,10 @@ +rm_conffile /etc/init/cloud-config.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-final.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-init-blocknet.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-init-container.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-init-local.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-init-nonet.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-init.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/init/cloud-log-shutdown.conf 0.7.9-243-ge74d775-0ubuntu2~ +rm_conffile /etc/NetworkManager/dispatcher.d/hook-network-manager 23.3~4ga31370af-0ubuntu1~ +rm_conffile /etc/cloud/clean.d/README 24.1~4~ diff --git a/debian/cloud-init-base.manpages b/debian/cloud-init-base.manpages new file mode 100644 index 00000000000..605cfd67af3 --- /dev/null +++ b/debian/cloud-init-base.manpages @@ -0,0 +1,3 @@ +doc/man/cloud-id.1 +doc/man/cloud-init-per.1 +doc/man/cloud-init.1 diff --git a/debian/cloud-init-base.postinst b/debian/cloud-init-base.postinst new file mode 100644 index 00000000000..f796b163e20 --- /dev/null +++ b/debian/cloud-init-base.postinst @@ -0,0 +1,329 @@ +#!/bin/sh + +set -e + +. /usr/share/debconf/confmodule + +set -f # disable pathname expansion +db_capb escape # to support carriage return / multi-line values + +debug() { + [ "${_CI_UPGRADE_DEBUG:-0}" = "0" ] && return 0 + echo "$@" 1>&2 || : +} + +update_cfg() { + # takes filename, header, new object (in yaml), optionally 'remover' + # and merges new into existing object in filename, and then updates file + # remover a string that means "delete existing entry" + python3 -c ' +import sys, yaml + +def update(src, cand): + if not (isinstance(src, dict) and isinstance(cand, dict)): + return cand + for k, v in cand.items(): + # if the candidate has _ as value, delete source + if v == REMOVER: + if k in src: + del src[k] + continue + if k not in src: + src[k] = v + else: + src[k] = update(src[k], v) + return src + +(fname, header, newyaml) = sys.argv[1:4] +REMOVER = object +if len(sys.argv) == 5: + REMOVER = sys.argv[4] +newcfg = yaml.safe_load(newyaml) + +with open(fname, "r") as fp: + cfg = yaml.safe_load(fp) +if not cfg: cfg = {} + +cfg = update(cfg, newcfg) + +with open(fname, "w") as fp: + fp.write(header + "\n") + fp.write(yaml.dump(cfg))' "$@" +} + +handle_preseed_maas() { + local cfg_file="/etc/cloud/cloud.cfg.d/90_dpkg_maas.cfg" + local md_url="" creds_all="" c_key="" t_key="" t_sec="" c_sec=""; + + db_get "cloud-init-base/maas-metadata-url" && md_url="$RET" || : + db_get "cloud-init-base/maas-metadata-credentials" && creds_all="$RET" || : + + # nothing to do + [ -n "$md_url" -o -n "$creds_all" ] || return 0 + + # change a url query string format into : delimited + if [ -n "$creds_all" -a "${creds_all#*&}" != "${creds_all}" ]; then + # the command here ends up looking like: + # python3 -c '...' 'oauth_consumer_key=v1&oauth_token_key=v2...' \ + # oauth_consumer_key oauth_token_key oauth_token_secret + creds_all=$(python3 -c 'from six.moves.urllib.parse import parse_qs; +import sys; +keys = parse_qs(sys.argv[1]) +for k in sys.argv[2:]: + sys.stdout.write("%s:" % keys.get(k,[""])[0])' "$creds_all" \ + oauth_consumer_key oauth_token_key oauth_token_secret +) + fi + + # now, if non-empty creds_all is: consumer_key:token_key:token_secret + if [ -n "$creds_all" ]; then + OIFS="$IFS"; IFS=:; set -- $creds_all; IFS="$OIFS" + c_key=$1; t_key=$2; t_sec=$3 + fi + + if [ "$md_url" = "_" -a "${c_key}:${t_key}:${t_sec}" = "_:_:_" ]; then + # if all these values were '_', the delete value, just delete the file. + rm -f "$cfg_file" + else + local header="# written by cloud-init-base debian package per preseed entries +# cloud-init-base/{maas-metadata-url,/maas-metadata-credentials}" + + local pair="" k="" v="" pload="" orig_umask="" + for pair in "metadata_url:$md_url" "consumer_key:${c_key}" \ + "token_key:${t_key}" "token_secret:$t_sec"; do + k=${pair%%:*} + v=${pair#${k}:} + [ -n "$v" ] && pload="${pload} $k: \"$v\"," + done + + # '_' would indicate "delete", otherwise, existing entries are left + orig_umask=$(umask) + umask 066 + : >> "$cfg_file" && chmod 600 "$cfg_file" + update_cfg "$cfg_file" "$header" "datasource: { MAAS: { ${pload%,} } }" _ + umask ${orig_umask} + fi + + # now clear the database of the values, as they've been consumed + db_unregister "cloud-init-base/maas-metadata-url" || : + db_unregister "cloud-init-base/maas-metadata-credentials" || : +} + +handle_preseed_local_cloud_config() { + local ccfg="" debconf_name="cloud-init-base/local-cloud-config" + local cfg_file="/etc/cloud/cloud.cfg.d/90_dpkg_local_cloud_config.cfg" + local header="# written by cloud-init-base debian package per preseed entry +# $debconf_name" + + db_get "${debconf_name}" && ccfg="$RET" || : + + if [ "$ccfg" = "_" ]; then + rm -f "$cfg_file" + elif [ -n "$ccfg" ]; then + { echo "$header"; echo "$ccfg"; } > "$cfg_file" + fi + db_unregister "${debconf_name}" || : +} + +fix_1336855() { + ### Begin fix for LP: 1336855 + # fix issue where cloud-init misidentifies the location of grub and + # where grub misidentifies the location of the device + + # if cloud-init's grub module did not run, then it did not break anything. + [ -f /var/lib/cloud/instance/sem/config_grub_dpkg ] || return 0 + + # This bug only happened on /dev/xvda devices + [ -b /dev/xvda ] || return 0 + + # we can't fix the system without /proc/cmdline + [ -r /proc/cmdline ] || return 0 + + # Don't do anything unless we have grub + command -v grub-install > /dev/null || return 0 + + # First, identify the kernel device for the parent. + for parm in $(cat /proc/cmdline); do + dev=$(echo $parm | awk -F\= '{print$NF}') + case $parm in + root=UUID*) [ -d /dev/disk/by-uuid ] && + root_dev=$(readlink -f /dev/disk/by-uuid/$dev);; + root=LABEL*) [ -d /dev/disk/by-label ] && + root_dev=$(readlink -f /dev/disk/by-label/$dev);; + root=/dev*) [ -d /dev ] && + root_dev=$(readlink -f $dev);; + esac + [ -n "$root_dev" ] && break + done + + # Don't continue if we don't have a root directive + [ -z "$root_dev" ] && return 0 + + # Only deal with simple, cloud-based devices + case $root_dev in + /dev/vda*|/dev/xvda*|/dev/sda*) ;; + *) return 0;; + esac + + # Make sure that we are not chrooted. + [ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" ] && return 0 + + # Check if we are in a container, i.e. LXC + if systemd-detect-virt --quiet --container || lxc-is-container 2>/dev/null; then + return 0 + fi + + # Find out where grub thinks the root device is. Only continue if + # grub postinst would install/reinstall grub + db_get grub-pc/install_devices && grub_cfg_dev=${RET} || return 0 + db_get grub-pc/install_devices_empty && grub_dev_empty=${RET} || return 0 + + # Find out the parent device for the root device. + # example output: sda/sda1 + block_path=$(udevadm info -q path -n $root_dev | awk '-Fblock/' '{print$NF}') || return 0 + + # Extract the parent device name. This works where the device is a block device + # example output: /dev/sda + parent_dev=$(echo $block_path | awk '-F/' '$1 { if ( $1 ) {print"/dev/"$1}}') + [ -b "${parent_dev}" ] || return 0 + + # Do nothing if the device that the grub postinst would install is already used + [ "$grub_cfg_dev" = "$parent_dev" -o "$grub_cfg_dev" = "$root_dev" ] && return 0 + + # If we get here, do the installation + echo "Reconfiguring grub install device due to mismatch (LP: #1336855)" + echo " Grub should use $parent_dev but is configured for $grub_cfg_dev" + db_set grub-pc/install_devices "$parent_dev" + grub-install $parent_dev && + echo "Reinstalled grub" || + echo "WARNING! Unable to fix grub device mismatch. You may be broken." + +} + +fix_lp1889555() { + local oldver="$1" last_bad_ver="20.3-2-g371b392c-0ubuntu1" + dpkg --compare-versions "$oldver" le-nl "$last_bad_ver" || return 0 + + # if cloud-init's grub module did not run, then it did not break anything. + [ -f /var/lib/cloud/instance/sem/config_grub_dpkg ] || return 0 + + # Don't do anything unless we have grub + command -v grub-install > /dev/null || return 0 + + # Make sure that we are not chrooted. + [ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" ] && return 0 + + # Check if we are in a container, i.e. LXC + if systemd-detect-virt --quiet --container || lxc-is-container 2>/dev/null; then + return 0 + fi + + # This bug only applies to NVMe devices + [ -e /dev/nvme0 ] || return 0 + + db_get grub-pc/install_devices && grub_cfg_dev=${RET} || return 0 + + # If the current setting is not the (potentially-incorrect) default we + # expect, this implies user intervention so leave things alone + [ "$grub_cfg_dev" = "/dev/sda" ] || return 0 + + correct_idev="$(python3 -c "import logging; from cloudinit.config.cc_grub_dpkg import fetch_idevs; print(fetch_idevs(logging.getLogger()))")" || return 0 + + # If correct_idev is the empty string, we failed to determine the correct + # install device; do nothing + [ -z "$correct_idev" ] && return 0 + + # If the correct_idev is already configured, do nothing + [ "$grub_cfg_dev" = "$correct_idev" ] && return 0 + + echo "Reconfiguring grub install device due to mismatch (LP: #1889555)" + echo " grub should use $correct_idev but is configured for $grub_cfg_dev" + db_set grub-pc/install_devices "$correct_idev" + db_set grub-pc/install_devices_empty "false" +} + +rename_hook_hotplug_udev_rule() { + # Avoids LP: #1946003 see commit: b519d861aff8b44a0610c176cb34adcbe28df144 + if [ -f /etc/udev/rules.d/10-cloud-init-hook-hotplug.rules ]; then + mv -f /etc/udev/rules.d/10-cloud-init-hook-hotplug.rules \ + /etc/udev/rules.d/90-cloud-init-hook-hotplug.rules + fi +} + +move_socket_fifo_to_share_dir() { + # This change arrived in cloud-init 25.1.3 + local old_fifo_dir="/run/cloud-init/" + local new_fifo_dir="/run/cloud-init/share/" + + # Move the socket fifo to /run/cloud-init/share + # The old location was /run/cloud-init/hook-hotplug-cmd + if [ -p "${old_fifo_dir}hook-hotplug-cmd" ]; then + # Ensure the share directory exists + mkdir -p "${new_fifo_dir}" + chmod 700 "${new_fifo_dir}" + + mv -f "${old_fifo_dir}hook-hotplug-cmd" "${new_fifo_dir}" + chmod 600 "${new_fifo_dir}hook-hotplug-cmd" + + invoke-rc.d cloud-init-hotplugd.socket restart || true + fi +} + +# Migrate a debconf value from cloud-init/* to cloud-init-base/* +# +# Arguments: +# Name of the debconf question +migrate_debconf_to_cloud_init_base() { + local old="cloud-init/$1" new="cloud-init-base/$1" + if db_get "$new"; then + # cloud-init-base/* win against cloud-init/* values + return 0 + fi + if db_get "$old"; then + echo "Migrating debconf value: ${old} -> ${new}" 1>&2 || : + db_set "$new" "$RET" + db_unregister "$old" || : + fi +} + +upgrade_to_cloud_init_base() { + migrate_debconf_to_cloud_init_base local-cloud-config + migrate_debconf_to_cloud_init_base maas-metadata-credentials + migrate_debconf_to_cloud_init_base maas-metadata-url + migrate_debconf_to_cloud_init_base datasources +} + +if [ "$1" = "configure" ]; then + upgrade_to_cloud_init_base "$2" + + if db_get cloud-init-base/datasources; then + values="$RET" + if [ "${values#*MaaS}" != "${values}" ]; then + # if db had old MAAS spelling, fix it. + values=$(echo "$values" | sed 's,MaaS,MAAS,g') + db_set cloud-init-base/datasources "$values" + fi + cat > /etc/cloud/cloud.cfg.d/90_dpkg.cfg <&2 + sed -i "s|^\(${first4}\)\(${remain}\)|\1,${adop}\2|" /etc/fstab + done +} + +convert_varlib_05x_06x() { + local url="http://169.254.169.254/2009-04-04/meta-data/instance-id" + local tout="--connect-timeout .5 --read-timeout .5" + local iid="" f="" uptime="" bn="" + iid=$(wget "${url}" ${tout} --tries 1 -O - 2>/dev/null) || iid="" + + [ -n "${iid}" -a -d /var/lib/cloud ] || return 0 + cd /var/lib/cloud + mkdir -p "instances/${iid}" "instances/${iid}/sem" + [ -e instance ] || ln -sf "instances/${iid}" instance + for f in data/*; do + [ -f "$f" ] || continue + case "${f#*/}" in + user-data.txt.i|user-data.txt|cloud-config.txt) + mv "${f}" instance/ + ;; + esac + done + [ -f data/cache/obj.pkl ] && mv data/cache/obj.pkl instance/ + for f in sem/*.${iid}; do + [ -f "${f}" ] || continue + bn=${f#*/}; bn=${bn%.${iid}} + case "${bn}" in + set_defaults) mv "${f}" "instance/sem/config-locale";; + set_hostname) mv "${f}" "instance/sem/config-set_hostname";; + *) mv "${f}" "instance/sem/${bn}";; + esac + done + [ ! -f sem/update_hostname.always ] || + mv sem/update_hostname.always sem/config-update_hostname.always + rmdir data/cache 2>/dev/null || : + rm -f data/available.build + + mkdir -p instance/scripts + if [ -d data/scripts ]; then + mv data/scripts/* instance/scripts || : + rmdir data/scripts || : + fi + [ -d data/scripts/* ] && mv data/scripts instance/ + + [ ! -e instance/boot-finished ] && + [ -f /proc/uptime ] && read uptime other instance/boot-finished || : + + return 0 +} + +azure_apply_new_instance_id_1506187() { + # With LP: #1506187, the Azure instance ID detection method was changed + # to use the DMI data. In order to prevent existing instances from thinking + # they are new instances, the instance ID needs to be updated here. + + if grep DataSourceAzure /var/lib/cloud/instance/datasource > /dev/null 2>&1; then + + product_id_f="/sys/devices/virtual/dmi/id/product_uuid" + instance_id_f="/var/lib/cloud/data/instance-id" + + if [ ! -e "${product_id_f}" -o ! -e "${instance_id_f}" ]; then + return 0 + fi + + # Get the current instance ID's (new and old) + new_instance_id="$(cat ${product_id_f})" + old_instance_id="$(cat ${instance_id_f})" + + if [ "${new_instance_id}" = "${old_instance_id}" ]; then + # this may have been applied for a prior version, i.e. upgrading + # from 14.04 to 16.04 + return 0 + + elif [ -z "${new_instance_id}" -o -z "${old_instance_id}" ]; then + cat < /var/lib/cloud/data/instance-id + + # Remove the symlink for the instance + rm /var/lib/cloud/instance + + # Rename the old instance id to the new one + mv /var/lib/cloud/instances/${old_instance_id} \ + /var/lib/cloud/instances/${new_instance_id} + + # Link the old id to the new one, just incase + ln -s /var/lib/cloud/instances/${new_instance_id} \ + /var/lib/cloud/instances/${old_instance_id} + + # Make the active instance the new id + ln -s /var/lib/cloud/instances/${new_instance_id} \ + /var/lib/cloud/instance + fi +fi +} + +cleanup_lp1552999() { + local oldver="$1" last_bad_ver="0.7.7~bzr1178" + dpkg --compare-versions "$oldver" le "$last_bad_ver" || return 0 + local hdir="/var/lib/systemd/deb-systemd-helper-enabled" + hdir="$hdir/multi-user.target.wants" + local edir="/etc/systemd/system/multi-user.target.wants" + rm -f "$hdir/cloud-config.service" "$hdir/cloud-final.service" \ + "$hdir/cloud-init-local.service" "$hdir/cloud-init.service" +} + +cleanup_oci_network_lp1956788() { + # Remove vestigial /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg + # from Oracle now that datasource honors system cfg above datasource cfg. + grep DataSourceOracle /var/lib/cloud/instance/datasource > /dev/null 2>&1 || return 0 + rm -f /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg +} + +case "$1" in + install|upgrade) + if dpkg --compare-versions "$2" le "0.5.11-0ubuntu1"; then + # rename the config entries in sem/ so they're not run again + + # transition names in 0.5.11 had only short name (no config- prefix) + # so create config- entries for each + for name in apt-update-upgrade disable-ec2-metadata mounts \ + puppet runcmd ssh updates-check; do + move_sem ${name} config-${name} + done + + # 0.5.11 split 'config-misc' into 'updates-check' and 'runcmd' + move_sem config-misc config-updates-check config-runcmd + fi + + if dpkg --compare-versions "$2" le "0.5.14-0ubuntu5"; then + fix_ephemeral0_micro + fi + + if dpkg --compare-versions "$2" le 0.6.0-0ubuntu1; then + # convert /var/lib/cloud from 0.5.x layout to 0.6.x + convert_varlib_05x_06x + fi + + # 0.6.0 changed 'user-scripts' to config-scripts-user (LP: #1049146) + if [ -e /var/lib/cloud/instance/sem/user-scripts ]; then + ln -sf user-scripts /var/lib/cloud/instance/sem/config-scripts-user + fi + + # 0.7.7-bzr1556 introduced new instance ID source for Azure + if dpkg --compare-versions "$2" le "0.7.7~bzr1556-0ubuntu1"; then + azure_apply_new_instance_id_1506187 + fi + + d=/etc/cloud/ + if [ -f "$d/distro.cfg" ] && [ ! -f "$d/cloud.cfg.d/90_dpkg.cfg" ]; then + echo "moving $d/distro.cfg -> $d/cloud.cfg.d/90_dpkg.cfg" + [ -d "${d}/cloud.cfg.d" ] || mkdir "${d}/cloud.cfg.d" + mv "$d/distro.cfg" "$d/cloud.cfg.d/90_dpkg.cfg" + fi + + cleanup_lp1552999 "$oldver" + cleanup_oci_network_lp1956788 + +esac + +#DEBHELPER# diff --git a/debian/cloud-init-base.prerm b/debian/cloud-init-base.prerm new file mode 100644 index 00000000000..8954fa4d7c2 --- /dev/null +++ b/debian/cloud-init-base.prerm @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e +rm -f /etc/cron.d/cloudinit-updates + +#DEBHELPER# diff --git a/debian/cloud-init-base.templates b/debian/cloud-init-base.templates new file mode 100644 index 00000000000..8ebff4352d6 --- /dev/null +++ b/debian/cloud-init-base.templates @@ -0,0 +1,21 @@ +Template: cloud-init-base/datasources +Type: multiselect +Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, VMware, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, Hetzner, IBMCloud, Oracle, Exoscale, RbxCloud, UpCloud, Vultr, LXD, NWCS, Akamai, WSL, CloudCIX, None +Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, VMware, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, Hetzner, IBMCloud, Oracle, Exoscale, RbxCloud, UpCloud, Vultr, LXD, NWCS, Akamai, WSL, CloudCIX, None +__Choices: NoCloud: Reads info from /var/lib/cloud/seed only, ConfigDrive: Reads data from Openstack Config Drive, OpenNebula: read from OpenNebula context disk, DigitalOcean: reads data from Droplet datasource, Azure: read from MS Azure cdrom. Requires walinux-agent, AltCloud: config disks for RHEVm and vSphere, VMware: reads data from guestinfo table or env vars, OVF: Reads data from OVF Transports, MAAS: Reads data from Ubuntu MAAS, GCE: google compute metadata service, OpenStack: native openstack metadata service, CloudSigma: metadata over serial for cloudsigma.com, SmartOS: Read from SmartOS metadata service, Bigstep: Bigstep metadata service, Scaleway: Scaleway metadata service, AliYun: Alibaba metadata service, Ec2: reads data from EC2 Metadata service, CloudStack: Read from CloudStack metadata service, Hetzner: Hetzner Cloud, IBMCloud: IBM Cloud. Previously softlayer or bluemix., Oracle: Oracle Compute Infrastructure, Exoscale: Exoscale, RbxCloud: HyperOne and Rootbox platforms, UpCloud: UpCloud, Vultr: Vultr Cloud, LXD: Reads /dev/lxd/sock representation of instance data, NWCS: NWCS, Akamai: Akamai and Linode platforms, WSL: Windows Subsystem for Linux, CloudCIX: Reads from CloudCIX metadata service, None: Failsafe datasource +_Description: Which data sources should be searched? + Cloud-init supports searching different "Data Sources" for information + that it uses to configure a cloud instance. + . + Warning: Only select 'Ec2' if this system will be run on a system with + the EC2 metadata service present. Doing so incorrectly will result in + a substantial timeout on boot. + . + Warning: If this system is intended to run on a cloud / platform + different than the original one, then make sure to install the correct + cloud-init package: + . + - Install a cloud-specific cloud-init package if the target platform + requires one. + - Install cloud-init, containing all dependencies, if the target is + unknown. diff --git a/debian/control b/debian/control new file mode 100644 index 00000000000..6cabc577553 --- /dev/null +++ b/debian/control @@ -0,0 +1,114 @@ +Source: cloud-init +Section: admin +Priority: optional +Homepage: https://cloud-init.io/ +Maintainer: Ubuntu Developers +Build-Depends: debhelper-compat (= 13), + dh-python, + meson, + pkgconf, + bash-completion, + iproute2, + po-debconf, + python3, + python3-configobj, + python3-debconf, + python3-jinja2, + python3-jsonpatch, + python3-jsonschema, + python3-mock, + python3-oauthlib, + python3-pytest, + python3-pytest-mock, + python3-requests, + python3-serial, + python3-yaml, + python3-responses, + python3-passlib, + udev, + systemd-dev +Vcs-Browser: https://github.com/canonical/cloud-init/tree/ubuntu/devel +Vcs-Git: https://github.com/canonical/cloud-init -b ubuntu/devel +Standards-Version: 4.5.0 +Rules-Requires-Root: no + +Package: cloud-init-base +Architecture: all +Depends: cloud-guest-utils | cloud-utils, + dhcpcd-base, + iproute2, + netcat-openbsd, + netplan.io, + procps, + python3, + python3-configobj, + python3-debconf, + python3-requests, + python3-jinja2, + python3-jsonpatch, + python3-jsonschema, + python3-oauthlib, + python3-yaml, + ${misc:Depends}, + ${python3:Depends} +Recommends: eatmydata, gdisk, gnupg, python3-apt, software-properties-common +Suggests: openssh-server, ssh-import-id +Replaces: cloud-init (<< 25.1.1-0ubuntu2~), cloud-init-base +Breaks: cloud-init (<< 25.1.1-0ubuntu2~), cloud-init-base +Description: initialization and customization tool for cloud instances + Cloud-init with minimal dependencies, refer to cloud-init for more + information. + +Package: cloud-init-azure +Architecture: all +Depends: cloud-init-base (= ${binary:Version}), + python3-passlib, + ${misc:Depends}, +Description: Azure specific cloud-init + This metapackage depends on cloud-init-base and additional packages for + Azure. + +Package: cloud-init-cloud-sigma +Architecture: all +Depends: cloud-init-base (= ${binary:Version}), + python3-serial, + ${misc:Depends}, +Description: Cloud Sigma specific cloud-init + This metapackage depends on cloud-init-base and additional packages for + Cloud Sigma. + +Package: cloud-init-smart-os +Architecture: all +Depends: cloud-init-base (= ${binary:Version}), + python3-serial, + ${misc:Depends}, +Description: Smart OS specific cloud-init + This metapackage depends on cloud-init-base and additional packages for + Smart OS. + +Package: cloud-init +Architecture: all +Depends: cloud-init-base (= ${binary:Version}), + python3-serial, + python3-passlib, + ${misc:Depends}, +Description: initialization and customization tool for cloud instances + Cloud-init is the industry standard multi-distribution method for + cross-platform cloud instance initialization. It is supported across all major + public cloud providers, provisioning systems for private cloud infrastructure, + and bare-metal installations. + . + Cloud instances are initialized from a disk image and instance data: + . + * Cloud metadata + * User data (optional) + * Vendor data (optional) + . + Cloud-init will identify the cloud it is running on during boot, read any + provided metadata from the cloud and initialize the system accordingly. This + may involve setting up the network and storage devices to configuring SSH + access key and many other aspects of a system. Later on the cloud-init will + also parse and process any optional user or vendor data that was passed to + the instance. + . + This is a metapackage that includes every cloud-init dependency. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000000..598cda14cff --- /dev/null +++ b/debian/copyright @@ -0,0 +1,40 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: cloud-init +Upstream-Contact: cloud-init-dev@lists.launchpad.net +Source: https://launchpad.net/cloud-init + +Files: * +Copyright: 2010, Canonical Ltd. +License: GPL-3 or Apache-2.0 + +License: GPL-3 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + The complete text of the GPL version 3 can be seen in + /usr/share/common-licenses/GPL-3. + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian-based systems the full text of the Apache version 2.0 license + can be found in `/usr/share/common-licenses/Apache-2.0'. diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 00000000000..9eb215def81 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,12 @@ +# Configuration file for "gbp " + +# See default config settings at: /etc/git-buildpackage/gbp.conf + +# Options only affecting gbp dch +[dch] +# options passed to git-log: +git-log = --no-merges --reverse +# Customizatons can e.g. be used for line wrapping +customizations=./debian/gbp_format_changelog +multimaint-merge = False +multimaint = False diff --git a/debian/gbp_format_changelog b/debian/gbp_format_changelog new file mode 100755 index 00000000000..42b89da1a07 --- /dev/null +++ b/debian/gbp_format_changelog @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# Simple changelog entry formatter +# +# It simply uses the built in formatter and linewraps the text +# +# Use git-dch --customizations=//scripts/gbp_format_changelog +# or set it via gbp.conf + +import re +import textwrap + +import gbp.dch + +# FILTER_UPSTREAM_COMMMITERS are authors for which to redact [author name] +# suffix per changelog message. The reason we redact those names as they +# will be repeated for most changelog entries. + +# To override this default filter behavior, copy this file into your project's +# ./debian/ directory and adapt as needed. + +FILTER_UPSTREAM_COMMITERS = ( # cloud-init upstream author names + "Chad Smith", + "James Falcon", + "Brett Holman", + "Alberto Contreras", +) + +FILTER_NOISY_COMMIT_REGEX = ( + r"update changelog.*", + r"refresh patches against.*", +) + +# To filter Jira tickets from "SC" project out of potential changelog comments +JIRA_PROJECT_KEY = "SC" + +UNWRAPPABLE_DELIMITERS = { + "]": "[", + ")": "(", +} + + +def _wrap_on_delimiter(text, prefix="", max_length=70) -> list: + """Break lines at specific UNWRAPPABLE_DELIMITERS. + + When a line ends with either (LP: #XXX) or [Author Name] avoid using + textwrap.wrap which breaks at the last whitespace. + + Instead break at the leading ( or [ special delimiter to ensure entire + author name or LP bug reference remains on the same line. + + Fallback to use textwrap.wrap if special conditions don't apply. + + Return a list of individual lines. + """ + if len(text) <= max_length: + return [prefix + text] + if text[-1] in UNWRAPPABLE_DELIMITERS: + delimiter = UNWRAPPABLE_DELIMITERS[text[-1]] + part1, sep, part2 = text.rpartition(delimiter) + lines = [] + for part in (part1.rstrip(), f"{sep}{part2}"): + if lines: + if len(lines[-1] + " " + part) < 70: + # Then the previous part plus current part should be joined + part = lines.pop() + " " + part + part = f" {part}" + lines.extend(_wrap_on_delimiter(part, prefix="" if lines else "+")) + return lines + return textwrap.wrap(prefix + text) + + +def format_changelog_entry(commit_info, options, last_commit=False): + entry = gbp.dch.format_changelog_entry(commit_info, options, last_commit) + if re.search(rf"\({JIRA_PROJECT_KEY}-\d+\)", entry[0]): + # Remove JIRA card references from debian/changelog comments + entry[0] = re.sub(r"\({JIRA_PROJECT_KEY}-\d+\)", "", entry[0]) + if commit_info["author"].name not in FILTER_UPSTREAM_COMMITERS: + # Only append non-upstream authors since most committers are upstream + entry.append(f"[{commit_info['author'].name}]") + if entry: + combined_entry = " ".join(entry) + for filter_re in FILTER_NOISY_COMMIT_REGEX: + if re.match(filter_re, combined_entry): + return None + return _wrap_on_delimiter(combined_entry, prefix="+") diff --git a/debian/po/POTFILES.in b/debian/po/POTFILES.in new file mode 100644 index 00000000000..35776935124 --- /dev/null +++ b/debian/po/POTFILES.in @@ -0,0 +1 @@ +[type: gettext/rfc822deb] cloud-init-base.templates diff --git a/debian/po/templates.pot b/debian/po/templates.pot new file mode 100644 index 00000000000..fc09c726c84 --- /dev/null +++ b/debian/po/templates.pot @@ -0,0 +1,246 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the cloud-init package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: cloud-init\n" +"Report-Msgid-Bugs-To: cloud-init@packages.debian.org\n" +"POT-Creation-Date: 2024-09-03 10:16+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "NoCloud: Reads info from /var/lib/cloud/seed only" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "ConfigDrive: Reads data from Openstack Config Drive" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "OpenNebula: read from OpenNebula context disk" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "DigitalOcean: reads data from Droplet datasource" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Azure: read from MS Azure cdrom. Requires walinux-agent" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "AltCloud: config disks for RHEVm and vSphere" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "OVF: Reads data from OVF Transports" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "MAAS: Reads data from Ubuntu MAAS" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "GCE: google compute metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "OpenStack: native openstack metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "CloudSigma: metadata over serial for cloudsigma.com" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "SmartOS: Read from SmartOS metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Bigstep: Bigstep metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Scaleway: Scaleway metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "AliYun: Alibaba metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Ec2: reads data from EC2 Metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "CloudStack: Read from CloudStack metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Hetzner: Hetzner Cloud" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "IBMCloud: IBM Cloud. Previously softlayer or bluemix." +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Oracle: Oracle Compute Infrastructure" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Exoscale: Exoscale" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "RbxCloud: HyperOne and Rootbox platforms" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "UpCloud: UpCloud" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "VMware: reads data from guestinfo table or env vars" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Vultr: Vultr Cloud" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "LXD: Reads /dev/lxd/sock representation of instance data" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "NWCS: NWCS" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "Akamai: Akamai and Linode platforms" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "WSL: Windows Subsystem for Linux" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "CloudCIX: Reads from CloudCIX metadata service" +msgstr "" + +#. Type: multiselect +#. Choices +#: ../cloud-init-base.templates:1001 +msgid "None: Failsafe datasource" +msgstr "" + +#. Type: multiselect +#. Description +#: ../cloud-init-base.templates:1002 +msgid "Which data sources should be searched?" +msgstr "" + +#. Type: multiselect +#. Description +#: ../cloud-init-base.templates:1002 +msgid "" +"Cloud-init supports searching different \"Data Sources\" for information " +"that it uses to configure a cloud instance." +msgstr "" + +#. Type: multiselect +#. Description +#: ../cloud-init-base.templates:1002 +msgid "" +"Warning: Only select 'Ec2' if this system will be run on a system with the " +"EC2 metadata service present. Doing so incorrectly will result in a " +"substantial timeout on boot." +msgstr "" + +#. Type: multiselect +#. Description +#: ../cloud-init-base.templates:1002 +msgid "" +"Warning: If this system is intended to run on a cloud / platform different " +"than the original one, then make sure to install the correct cloud-init " +"package:" +msgstr "" + +#. Type: multiselect +#. Description +#: ../cloud-init-base.templates:1002 +msgid "" +"- Install a cloud-specific cloud-init package if the target platform\n" +" requires one.\n" +"- Install cloud-init, containing all dependencies, if the target is\n" +" unknown." +msgstr "" diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000000..e5c650d4674 --- /dev/null +++ b/debian/rules @@ -0,0 +1,28 @@ +#!/usr/bin/make -f + +include /usr/share/dpkg/pkg-info.mk + +%: + dh $@ --with python3 --buildsystem meson + +override_dh_auto_configure: + dh_auto_configure -- -Dinit_system=systemd -Dlibexecdir=lib -Ddistro_templates=chef_client.rb.tmpl,chrony.conf.ubuntu.tmpl,hosts.debian.tmpl,ntp.conf.ubuntu.tmpl,sources.list.ubuntu.deb822.tmpl,sources.list.ubuntu.deb822.tmpl,timesyncd.conf.tmpl + +override_dh_auto_test: +ifeq (,$(findstring nocheck,$(DEB_BUILD_OPTIONS))) + http_proxy= make PYVER=python3 check +else + @echo check disabled by DEB_BUILD_OPTIONS=$(DEB_BUILD_OPTIONS) +endif + +override_dh_installsystemd: + dh_installsystemd --no-restart-on-upgrade --no-start + +override_dh_auto_install: + dh_auto_install --destdir=debian/cloud-init-base + install -D -m 0644 ./tools/21-cloudinit.conf debian/cloud-init-base/etc/rsyslog.d/21-cloudinit.conf + install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init-base/etc/profile.d/Z99-cloud-locale-test.sh + install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init-base/etc/profile.d/Z99-cloudinit-warnings.sh + install -m 0644 -D debian/apport-general-hook.py debian/cloud-init-base/usr/share/apport/general-hooks/cloud-init.py + install -m 0644 -D debian/apport-launcher.py debian/cloud-init-base/usr/share/apport/package-hooks/cloud-init.py + flist=$$(find $(CURDIR)/debian/ -type f -name version.py) && sed -i 's,@@PACKAGED_VERSION@@,$(DEB_VERSION),' $${flist:-did-not-find-version-py-for-replacement} diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000000..163aaf8d82b --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/source/lintian-overrides b/debian/source/lintian-overrides new file mode 100644 index 00000000000..5b8bf5cb8f9 --- /dev/null +++ b/debian/source/lintian-overrides @@ -0,0 +1,2 @@ +# Silence lintian bug: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1014584 +cloud-init source: binary-nmu-debian-revision-in-source diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 00000000000..239532a97b9 --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,5 @@ +Name: cloud-init +Contact: James Falcon +Repository: https://github.com/canonical/cloud-init.git +Repository-Browse: https://github.com/canonical/cloud-init +Bug-Submit: https://bugs.launchpad.net/cloud-init diff --git a/debian/watch b/debian/watch new file mode 100644 index 00000000000..0f7a600b4c5 --- /dev/null +++ b/debian/watch @@ -0,0 +1,2 @@ +version=3 +https://launchpad.net/cloud-init/+download .*/\+download/cloud-init-(.+)\.tar.gz From 87cb037bdeb9f2ce9f74451eababb55e096b450c Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 19 Sep 2025 23:06:14 -0600 Subject: [PATCH 138/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index e815e02a657..15f5f96c657 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -25,7 +25,7 @@ concurrency: defaults: run: - shell: bash -e {0} + shell: bash -ex {0} # Note: No need to specify the shell option in the shellcheck command # as shellcheck reads and uses the shebang at the top of the linted scripts. From 3f58c867b5b83d4ae269dc2d3e5774b8a04e90fe Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Fri, 19 Sep 2025 23:20:07 -0600 Subject: [PATCH 139/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index 15f5f96c657..e815e02a657 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -25,7 +25,7 @@ concurrency: defaults: run: - shell: bash -ex {0} + shell: bash -e {0} # Note: No need to specify the shell option in the shellcheck command # as shellcheck reads and uses the shebang at the top of the linted scripts. From 12dda1c6ab95834a88ccd1cd569aa67b9f794213 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 22 Sep 2025 11:08:58 -0600 Subject: [PATCH 140/150] review comments chad --- .../workflows/shellcheck-debian-scripts.yml | 65 +++---------------- 1 file changed, 10 insertions(+), 55 deletions(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index e815e02a657..f253529f893 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -1,10 +1,10 @@ -name: ShellCheck Debian package scripts, error on new lints +name: ShellCheck Debian package scripts env: # This is a space separated string for multiple globs # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` # Assumption in this workflow: the resolved filepaths do not contain spaces. - GLOBS_TO_SHELLCHECK: "debian/cherry-pick debian/*.config debian/*.postinst debian/*.postrm debian/*.preinst debian/*.prerm packages/debian/*.postrm" + GLOBS_TO_SHELLCHECK: "**/*.postinst **/*.postrm **/*.preinst **/*.prerm" on: pull_request: @@ -23,10 +23,6 @@ concurrency: group: 'ci-${{ github.workflow }}-${{ github.ref }}' cancel-in-progress: true -defaults: - run: - shell: bash -e {0} - # Note: No need to specify the shell option in the shellcheck command # as shellcheck reads and uses the shebang at the top of the linted scripts. jobs: @@ -49,60 +45,19 @@ jobs: env: ALL_CHANGED_FILES: ${{ steps.matching-changed-files.outputs.all_changed_and_modified_files }} run: | + shopt -s globstar if [ -z "${ALL_CHANGED_FILES}" ] then echo "There are no changed files in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" else echo "The changed and modified (including deleted) files are ${ALL_CHANGED_FILES}" echo "The globs used are ${GLOBS_TO_SHELLCHECK}" - - # Define high-water marks for expected lint info and warn levels - declare -A INFO_LIMITS WARN_LIMITS - INFO_LIMITS["debian/cherry-pick"]=5 - WARN_LIMITS["debian/cherry-pick"]=4 - INFO_LIMITS["debian/cloud-init-base.config"]=3 - WARN_LIMITS["debian/cloud-init-base.config"]=7 - INFO_LIMITS["debian/cloud-init-base.postinst"]=16 - WARN_LIMITS["debian/cloud-init-base.postinst"]=17 - INFO_LIMITS["debian/cloud-init-base.preinst"]=15 - WARN_LIMITS["debian/cloud-init-base.preinst"]=21 - - ALL_NEW_LINTS="" - for script_name in ${ALL_CHANGED_FILES}; do - NEW_SCRIPT_LINTS="" - if [ -f ${script_name} ]; then - # Only error when lint counts are greater than limits - info_limit=0 - warn_limit=0 - # Respect and defined limits, otherwise exit non-zero - if [[ -v INFO_LIMITS[$script_name] ]]; then - info_limit=${INFO_LIMITS[$script_name]} - fi - if [[ -v WARN_LIMITS[$script_name] ]]; then - warn_limit=${WARN_LIMITS[$script_name]} - fi - shellcheck ${script_name} > output.txt || true - lint_info_count=$( grep -c '(info)' output.txt ) || true - lint_warn_count=$( grep -c '(warning)' output.txt ) || true - lint_error_count=$( grep -c '(error)' output.txt ) || true - if [ $lint_error_count -gt 0 ]; then - NEW_SCRIPT_LINTS+="shellcheck $script_name contains errors.\n" - fi - if [ $lint_info_count -gt $info_limit ]; then - NEW_SCRIPT_LINTS+="shellcheck $script_name- too many (info) lints: $lint_info_count\n" - fi - if [ $lint_warn_count -gt $warn_limit ]; then - NEW_SCRIPT_LINTS+="shellcheck $script_name- too many (warning) lints: $lint_warn_count\n" - fi - fi - if [ -n "$NEW_SCRIPT_LINTS" ]; then - ALL_NEW_LINTS+=$NEW_SCRIPT_LINTS - ALL_NEW_LINTS+=$(cat output.txt) - fi - done - if [ -n "${ALL_NEW_LINTS}" ]; then - echo -e "${ALL_NEW_LINTS}" - exit 1 + RETAINED_CHANGED_FILES=$(git ls-files ${ALL_CHANGED_FILES} | tr '\n' ' ') #filter out deleted files + echo "The matching and changed files are ${RETAINED_CHANGED_FILES}" + if [ -z "${RETAINED_CHANGED_FILES}" ] + then + echo "There are no changed files remaining in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" + else + shellcheck ${RETAINED_CHANGED_FILES} fi fi - echo "SUCCESS: no new shellcheck lints encountered" From 07816720a2095fecb33508dce401f8b3cf4a05c1 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 22 Sep 2025 11:10:25 -0600 Subject: [PATCH 141/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index f253529f893..2b4f60f6d16 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -1,7 +1,7 @@ name: ShellCheck Debian package scripts env: - # This is a space separated string for multiple globs + # This is a space separated string for multiple globs chad # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` # Assumption in this workflow: the resolved filepaths do not contain spaces. GLOBS_TO_SHELLCHECK: "**/*.postinst **/*.postrm **/*.preinst **/*.prerm" From 5db3e92ac8ef8a1424a4ac0e4a5989e10c3aad62 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 22 Sep 2025 11:11:47 -0600 Subject: [PATCH 142/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index 2b4f60f6d16..76411e6de8c 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -4,7 +4,7 @@ env: # This is a space separated string for multiple globs chad # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` # Assumption in this workflow: the resolved filepaths do not contain spaces. - GLOBS_TO_SHELLCHECK: "**/*.postinst **/*.postrm **/*.preinst **/*.prerm" + GLOBS_TO_SHELLCHECK: "debian/cherry-pick debian/*.config debian/*.postinst debian/*.postrm debian/*.preinst debian/*.prerm packages/debian/*.postrm" on: pull_request: From a4dfe06d1e62da15f42897b96367c97081cf3ecf Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 22 Sep 2025 11:12:04 -0600 Subject: [PATCH 143/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index 76411e6de8c..4434aa83631 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -15,9 +15,6 @@ on: # then the second PR will show these checks that were triggered by the first PR but not the second PR. branches: - 'ubuntu/**' - push: - branches: - - 'ubuntu/**' concurrency: group: 'ci-${{ github.workflow }}-${{ github.ref }}' From 3ba28815a34e829a2cd554381f935d98c8be1ad2 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 22 Sep 2025 11:13:03 -0600 Subject: [PATCH 144/150] wip --- debian/cherry-pick | 197 ------------------- debian/cloud-init-base.postinst | 329 -------------------------------- 2 files changed, 526 deletions(-) delete mode 100755 debian/cherry-pick delete mode 100644 debian/cloud-init-base.postinst diff --git a/debian/cherry-pick b/debian/cherry-pick deleted file mode 100755 index 0a5cea39b34..00000000000 --- a/debian/cherry-pick +++ /dev/null @@ -1,197 +0,0 @@ -#!/bin/bash - -VERBOSITY=0 -TEMP_D="" -CR=$'\n' - -error() { echo "$@" 1>&2; } -fail() { [ $# -eq 0 ] || error "$@"; exit 1; } - -Usage() { - cat <> - - Cherry pick a patch into debian/patches. - Useful to grab an upstream commit to the current packaging branch. - - options: - -h | --help show help -EOF -} - -bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; } -cleanup() { - [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}" -} - -debug() { - local level=${1}; shift; - [ "${level}" -gt "${VERBOSITY}" ] && return - error "${@}" -} - -shorten() { - local name="$1" len="70" - while [ "${#name}" -gt "$len" ]; do - name="${name%-*}" - done - _RET="$name" -} - -print_commit() { - local subject="$1" author="$2" bugs="$3" aname="" - aname=${author% <*} - echo "$subject${bugs:+ (LP: ${bugs})}" -} - -print_bugs() { - local subject="$1" author="$2" bugs="$3" aname="" - echo "$bugs" -} - -git_log_to_dch() { - # call printer with subject, author and bugs as extracted - # from either git format-patch output or git show output. - local line="" commit="" lcommit="" bugs="" - local printer="${1:-print_commit}" - while :; do - read line || break - case "$line" in - commit\ *|From\ *) - if [ -n "$commit" ]; then - "$printer" "$subject" "$author" "$bugs" - fi - commit=${line#* } - commit=${commit%% *} - bugs="" - author="" - subject="" - ;; - Author:\ *|From:\ *) author="${line#*: }";; - LP:*) bugs="${bugs:+${bugs}, }${line#*: }";; - "") [ -z "$subject" ] && read subject;; - Subject:\ *) - subject="${line#Subject: }" - subject="${subject#\[PATCH\] }" - ;; - esac - done - if [ -n "$commit" ]; then - "$printer" "$subject" "$author" "$bugs" - fi -} - -main() { - local short_opts="ho:v" - local long_opts="help,verbose" - local getopt_out="" - getopt_out=$(getopt --name "${0##*/}" \ - --options "${short_opts}" --long "${long_opts}" -- "$@") && - eval set -- "${getopt_out}" || - { bad_Usage; return; } - - local cur="" next="" - - while [ $# -ne 0 ]; do - cur="$1"; next="$2"; - case "$cur" in - -h|--help) Usage ; exit 0;; - -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));; - --) shift; break;; - esac - shift; - done - - [ -n "$TEMP_D" ] || - TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") || - { error "failed to make tempdir"; return 1; } - trap cleanup EXIT - - [ $# -gt 0 ] || { bad_Usage "must provide commit-ish"; return; } - - local r="" commit_in="$1" chash="" shash="" sname="" fname="" cur_br="" - cur_br=$(git rev-parse --abbrev-ref HEAD) || - { error "failed to get current branch"; return 1; } - chash=$(git show --quiet "--pretty=format:%H" "${commit_in}") || - { error "failed git show $commit_in"; return 1; } - - if git merge-base --is-ancestor "$chash" HEAD; then - error "current branch '$cur_br' already contains $commit_in ($chash)" - return 1 - fi - - out=$(git show --quiet "--pretty=format:%h %f" "$chash") || - { error "failed git show $chash"; return 1; } - - shash=${out% *} - sname=${out#* } - longname="cpick-$shash-$sname" - shorten "$longname" - fname="$_RET" - - [ -d debian/patches ] || mkdir -p debian/patches || - { error "failed to make debian/patches"; return 1; } - - local series="debian/patches/series" fpath="debian/patches/$fname" - if [ -e "$series" ] && out=$(grep -- "-${shash}-" "$series"); then - error "$chash already exists in $series" - error " $out" - return 1 - fi - - if [ -e "$series" ]; then - if out=$(quilt applied 2>&1); then - error "there are quilt patches applied!" - error "$out" - return 1 - fi - fi - - git format-patch --stdout -1 "$chash" > "$fpath" || - { error "failed git format-patch -1 $chash > $fpath"; return 1; } - - echo "$fname" >> "$series" || - { error "failed to write to $series"; return 1; } - - quilt push "$fname" || - { error "patches do not cleanly apply"; return 1; } - quilt refresh && quilt pop -a || - { error "failed to refresh or pop quilt"; return 1; } - - local message="" - message=$(git_log_to_dch < "$fpath") || - { error "failed getting log entry from $fpath"; return 1; } - dch -i "cherry-pick $shash: $message" - - dch -e || { - r=$?; - error "dch -e exited $r"; - return $r; - } - - local commit_files="" - commit_files=( "$series" "$fpath" ) - git diff HEAD "${commit_files[@]}" - - echo -n "Commit this change? (Y/n): " - read answer || fail "failed to read answer" - case "$answer" in - n|[Nn][oO]) exit 1;; - esac - - bugs=$(git_log_to_dch print_bugs < "$fpath") - msg="cherry pick $shash${bugs:+${CR}${CR}LP: ${bugs}}" - git add "$series" "$fpath" || - { error "failed to git add $series $fpath"; return 1; } - - git commit -m "$msg" "${commit_files[@]}" || - fail "failed to commit '$msg'" - - git commit -m "update changelog" debian/changelog || - fail "failed to commit update to debian changelog." - - return 0 -} - -main "$@" -# vi: ts=4 expandtab diff --git a/debian/cloud-init-base.postinst b/debian/cloud-init-base.postinst deleted file mode 100644 index f796b163e20..00000000000 --- a/debian/cloud-init-base.postinst +++ /dev/null @@ -1,329 +0,0 @@ -#!/bin/sh - -set -e - -. /usr/share/debconf/confmodule - -set -f # disable pathname expansion -db_capb escape # to support carriage return / multi-line values - -debug() { - [ "${_CI_UPGRADE_DEBUG:-0}" = "0" ] && return 0 - echo "$@" 1>&2 || : -} - -update_cfg() { - # takes filename, header, new object (in yaml), optionally 'remover' - # and merges new into existing object in filename, and then updates file - # remover a string that means "delete existing entry" - python3 -c ' -import sys, yaml - -def update(src, cand): - if not (isinstance(src, dict) and isinstance(cand, dict)): - return cand - for k, v in cand.items(): - # if the candidate has _ as value, delete source - if v == REMOVER: - if k in src: - del src[k] - continue - if k not in src: - src[k] = v - else: - src[k] = update(src[k], v) - return src - -(fname, header, newyaml) = sys.argv[1:4] -REMOVER = object -if len(sys.argv) == 5: - REMOVER = sys.argv[4] -newcfg = yaml.safe_load(newyaml) - -with open(fname, "r") as fp: - cfg = yaml.safe_load(fp) -if not cfg: cfg = {} - -cfg = update(cfg, newcfg) - -with open(fname, "w") as fp: - fp.write(header + "\n") - fp.write(yaml.dump(cfg))' "$@" -} - -handle_preseed_maas() { - local cfg_file="/etc/cloud/cloud.cfg.d/90_dpkg_maas.cfg" - local md_url="" creds_all="" c_key="" t_key="" t_sec="" c_sec=""; - - db_get "cloud-init-base/maas-metadata-url" && md_url="$RET" || : - db_get "cloud-init-base/maas-metadata-credentials" && creds_all="$RET" || : - - # nothing to do - [ -n "$md_url" -o -n "$creds_all" ] || return 0 - - # change a url query string format into : delimited - if [ -n "$creds_all" -a "${creds_all#*&}" != "${creds_all}" ]; then - # the command here ends up looking like: - # python3 -c '...' 'oauth_consumer_key=v1&oauth_token_key=v2...' \ - # oauth_consumer_key oauth_token_key oauth_token_secret - creds_all=$(python3 -c 'from six.moves.urllib.parse import parse_qs; -import sys; -keys = parse_qs(sys.argv[1]) -for k in sys.argv[2:]: - sys.stdout.write("%s:" % keys.get(k,[""])[0])' "$creds_all" \ - oauth_consumer_key oauth_token_key oauth_token_secret -) - fi - - # now, if non-empty creds_all is: consumer_key:token_key:token_secret - if [ -n "$creds_all" ]; then - OIFS="$IFS"; IFS=:; set -- $creds_all; IFS="$OIFS" - c_key=$1; t_key=$2; t_sec=$3 - fi - - if [ "$md_url" = "_" -a "${c_key}:${t_key}:${t_sec}" = "_:_:_" ]; then - # if all these values were '_', the delete value, just delete the file. - rm -f "$cfg_file" - else - local header="# written by cloud-init-base debian package per preseed entries -# cloud-init-base/{maas-metadata-url,/maas-metadata-credentials}" - - local pair="" k="" v="" pload="" orig_umask="" - for pair in "metadata_url:$md_url" "consumer_key:${c_key}" \ - "token_key:${t_key}" "token_secret:$t_sec"; do - k=${pair%%:*} - v=${pair#${k}:} - [ -n "$v" ] && pload="${pload} $k: \"$v\"," - done - - # '_' would indicate "delete", otherwise, existing entries are left - orig_umask=$(umask) - umask 066 - : >> "$cfg_file" && chmod 600 "$cfg_file" - update_cfg "$cfg_file" "$header" "datasource: { MAAS: { ${pload%,} } }" _ - umask ${orig_umask} - fi - - # now clear the database of the values, as they've been consumed - db_unregister "cloud-init-base/maas-metadata-url" || : - db_unregister "cloud-init-base/maas-metadata-credentials" || : -} - -handle_preseed_local_cloud_config() { - local ccfg="" debconf_name="cloud-init-base/local-cloud-config" - local cfg_file="/etc/cloud/cloud.cfg.d/90_dpkg_local_cloud_config.cfg" - local header="# written by cloud-init-base debian package per preseed entry -# $debconf_name" - - db_get "${debconf_name}" && ccfg="$RET" || : - - if [ "$ccfg" = "_" ]; then - rm -f "$cfg_file" - elif [ -n "$ccfg" ]; then - { echo "$header"; echo "$ccfg"; } > "$cfg_file" - fi - db_unregister "${debconf_name}" || : -} - -fix_1336855() { - ### Begin fix for LP: 1336855 - # fix issue where cloud-init misidentifies the location of grub and - # where grub misidentifies the location of the device - - # if cloud-init's grub module did not run, then it did not break anything. - [ -f /var/lib/cloud/instance/sem/config_grub_dpkg ] || return 0 - - # This bug only happened on /dev/xvda devices - [ -b /dev/xvda ] || return 0 - - # we can't fix the system without /proc/cmdline - [ -r /proc/cmdline ] || return 0 - - # Don't do anything unless we have grub - command -v grub-install > /dev/null || return 0 - - # First, identify the kernel device for the parent. - for parm in $(cat /proc/cmdline); do - dev=$(echo $parm | awk -F\= '{print$NF}') - case $parm in - root=UUID*) [ -d /dev/disk/by-uuid ] && - root_dev=$(readlink -f /dev/disk/by-uuid/$dev);; - root=LABEL*) [ -d /dev/disk/by-label ] && - root_dev=$(readlink -f /dev/disk/by-label/$dev);; - root=/dev*) [ -d /dev ] && - root_dev=$(readlink -f $dev);; - esac - [ -n "$root_dev" ] && break - done - - # Don't continue if we don't have a root directive - [ -z "$root_dev" ] && return 0 - - # Only deal with simple, cloud-based devices - case $root_dev in - /dev/vda*|/dev/xvda*|/dev/sda*) ;; - *) return 0;; - esac - - # Make sure that we are not chrooted. - [ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" ] && return 0 - - # Check if we are in a container, i.e. LXC - if systemd-detect-virt --quiet --container || lxc-is-container 2>/dev/null; then - return 0 - fi - - # Find out where grub thinks the root device is. Only continue if - # grub postinst would install/reinstall grub - db_get grub-pc/install_devices && grub_cfg_dev=${RET} || return 0 - db_get grub-pc/install_devices_empty && grub_dev_empty=${RET} || return 0 - - # Find out the parent device for the root device. - # example output: sda/sda1 - block_path=$(udevadm info -q path -n $root_dev | awk '-Fblock/' '{print$NF}') || return 0 - - # Extract the parent device name. This works where the device is a block device - # example output: /dev/sda - parent_dev=$(echo $block_path | awk '-F/' '$1 { if ( $1 ) {print"/dev/"$1}}') - [ -b "${parent_dev}" ] || return 0 - - # Do nothing if the device that the grub postinst would install is already used - [ "$grub_cfg_dev" = "$parent_dev" -o "$grub_cfg_dev" = "$root_dev" ] && return 0 - - # If we get here, do the installation - echo "Reconfiguring grub install device due to mismatch (LP: #1336855)" - echo " Grub should use $parent_dev but is configured for $grub_cfg_dev" - db_set grub-pc/install_devices "$parent_dev" - grub-install $parent_dev && - echo "Reinstalled grub" || - echo "WARNING! Unable to fix grub device mismatch. You may be broken." - -} - -fix_lp1889555() { - local oldver="$1" last_bad_ver="20.3-2-g371b392c-0ubuntu1" - dpkg --compare-versions "$oldver" le-nl "$last_bad_ver" || return 0 - - # if cloud-init's grub module did not run, then it did not break anything. - [ -f /var/lib/cloud/instance/sem/config_grub_dpkg ] || return 0 - - # Don't do anything unless we have grub - command -v grub-install > /dev/null || return 0 - - # Make sure that we are not chrooted. - [ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" ] && return 0 - - # Check if we are in a container, i.e. LXC - if systemd-detect-virt --quiet --container || lxc-is-container 2>/dev/null; then - return 0 - fi - - # This bug only applies to NVMe devices - [ -e /dev/nvme0 ] || return 0 - - db_get grub-pc/install_devices && grub_cfg_dev=${RET} || return 0 - - # If the current setting is not the (potentially-incorrect) default we - # expect, this implies user intervention so leave things alone - [ "$grub_cfg_dev" = "/dev/sda" ] || return 0 - - correct_idev="$(python3 -c "import logging; from cloudinit.config.cc_grub_dpkg import fetch_idevs; print(fetch_idevs(logging.getLogger()))")" || return 0 - - # If correct_idev is the empty string, we failed to determine the correct - # install device; do nothing - [ -z "$correct_idev" ] && return 0 - - # If the correct_idev is already configured, do nothing - [ "$grub_cfg_dev" = "$correct_idev" ] && return 0 - - echo "Reconfiguring grub install device due to mismatch (LP: #1889555)" - echo " grub should use $correct_idev but is configured for $grub_cfg_dev" - db_set grub-pc/install_devices "$correct_idev" - db_set grub-pc/install_devices_empty "false" -} - -rename_hook_hotplug_udev_rule() { - # Avoids LP: #1946003 see commit: b519d861aff8b44a0610c176cb34adcbe28df144 - if [ -f /etc/udev/rules.d/10-cloud-init-hook-hotplug.rules ]; then - mv -f /etc/udev/rules.d/10-cloud-init-hook-hotplug.rules \ - /etc/udev/rules.d/90-cloud-init-hook-hotplug.rules - fi -} - -move_socket_fifo_to_share_dir() { - # This change arrived in cloud-init 25.1.3 - local old_fifo_dir="/run/cloud-init/" - local new_fifo_dir="/run/cloud-init/share/" - - # Move the socket fifo to /run/cloud-init/share - # The old location was /run/cloud-init/hook-hotplug-cmd - if [ -p "${old_fifo_dir}hook-hotplug-cmd" ]; then - # Ensure the share directory exists - mkdir -p "${new_fifo_dir}" - chmod 700 "${new_fifo_dir}" - - mv -f "${old_fifo_dir}hook-hotplug-cmd" "${new_fifo_dir}" - chmod 600 "${new_fifo_dir}hook-hotplug-cmd" - - invoke-rc.d cloud-init-hotplugd.socket restart || true - fi -} - -# Migrate a debconf value from cloud-init/* to cloud-init-base/* -# -# Arguments: -# Name of the debconf question -migrate_debconf_to_cloud_init_base() { - local old="cloud-init/$1" new="cloud-init-base/$1" - if db_get "$new"; then - # cloud-init-base/* win against cloud-init/* values - return 0 - fi - if db_get "$old"; then - echo "Migrating debconf value: ${old} -> ${new}" 1>&2 || : - db_set "$new" "$RET" - db_unregister "$old" || : - fi -} - -upgrade_to_cloud_init_base() { - migrate_debconf_to_cloud_init_base local-cloud-config - migrate_debconf_to_cloud_init_base maas-metadata-credentials - migrate_debconf_to_cloud_init_base maas-metadata-url - migrate_debconf_to_cloud_init_base datasources -} - -if [ "$1" = "configure" ]; then - upgrade_to_cloud_init_base "$2" - - if db_get cloud-init-base/datasources; then - values="$RET" - if [ "${values#*MaaS}" != "${values}" ]; then - # if db had old MAAS spelling, fix it. - values=$(echo "$values" | sed 's,MaaS,MAAS,g') - db_set cloud-init-base/datasources "$values" - fi - cat > /etc/cloud/cloud.cfg.d/90_dpkg.cfg < Date: Mon, 22 Sep 2025 11:14:26 -0600 Subject: [PATCH 145/150] wip --- debian/cloud-init-base.config | 99 ----------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 debian/cloud-init-base.config diff --git a/debian/cloud-init-base.config b/debian/cloud-init-base.config deleted file mode 100644 index 43c658f23ab..00000000000 --- a/debian/cloud-init-base.config +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/sh - -set -e - -. /usr/share/debconf/confmodule - -hasEc2Md() { - local d=/var/lib/cloud i=/var/lib/cloud/instance/ f="" - local flist="${i}/datasource ${i}/obj.pkl ${d}/cache/obj.pkl" - # search current instance data source information - for f in ${flist}; do - [ -f "${f}" ] || continue - grep -q DataSourceEc2 "${f}" 2>/dev/null && return 0 - done - - - # there was no data above found that would have been indicated - # by a upgrade. lets just see if we can't find the metadata - # service. if wget is not present (it is not 'essential') - # then we will quietly return 1 - local url="http://169.254.169.254/2009-04-04/meta-data/instance-id" - i="" - if command -v wget >/dev/null 2>&1; then - local tout="--connect-timeout 5 --read-timeout 5 --tries 1" - i=$(wget "${url}" ${tout} -O - 2>/dev/null) || : - elif command -v curl >/dev/null 2>&1; then - i=$(curl "${url}" --max-time 1) || : - fi - # if instance-id starts with 'i-' then in all likelyhood its good - [ "${i#i-}" != "${i}" ] && return 0 - return 1 -} - -get_yaml_list() { - # get_yaml_list(file, key, def): return a comma delimited list with the value - # for the yaml array defined in 'key' from 'file'. if not found , return 'def' - # only really supports 'key: [ en1, en2 ]' or 'key: [en1, en2]' formats. - local file="$1" key="$2" default="$3" - [ -f "$file" ] || return 1 - # strip all whitespace, delete lines not matching key:, - # strip key: and [] and replace ',' with ', ' - RET="$(sed -e "s/\s//g" -e "/^$key:/"'!'d \ - -e "s/$key:\[//" -e "s/]//" -e "s/,/, /g" "$file")" - [ -n "$RET" ] || RET="$default" -} - -# Migrate a debconf value from cloud-init/* to cloud-init-base/* -# -# Arguments: -# Name of the debconf question -migrate_debconf_to_cloud_init_base() { - local old="cloud-init/$1" new="cloud-init-base/$1" - if db_get "$new"; then - # cloud-init-base/* win against cloud-init/* values - return 0 - fi - if db_get "$old"; then - echo "Migrating debconf value: ${old} -> ${new}" 1>&2 || : - db_set "$new" "$RET" - db_unregister "$old" || : - fi -} - -# old_dpkg_cfg is very old file that is no longer read by cloud-init. -# it gets re-named to cloud.cfg.d/90_dpkg.cfg in the preinst. -dpkg_cfg="/etc/cloud/cloud.cfg.d/90_dpkg.cfg" -old_dpkg_cfg="/etc/cloud/distro.cfg" -if [ -f "${old_dpkg_cfg}" -a ! -f "$dpkg_cfg" ]; then - dpkg_cfg="${old_dpkg_cfg}" - echo "WARN: reading value from ${old_dpkg_cfg}" 1>&2 -fi - -migrate_debconf_to_cloud_init_base datasources - -if [ -f "$dpkg_cfg" ]; then - if get_yaml_list "$dpkg_cfg" datasource_list NOTFOUND && - val="$RET" && [ "$val" != "NOTFOUND" ]; then - db_set cloud-init-base/datasources "$val" - else - echo "WARN: failed to read datasource_list from $dpkg_cfg" 1>&2 - fi -elif { db_fget cloud-init-base/datasources seen || : ; } && - [ "${RET}" = "false" ]; then - # this is the first time this we've run (installation or re-install after - # purge). try to determine if the Ec2 datasource is there. - # if it is, and Ec2 was not in the default list, then add it. - db_get cloud-init-base/datasources - def="${RET}" - case " ${def}," in - *\ Ec2,*) :;; - *) hasEc2Md && - db_set cloud-init-base/datasources "${def:+${def}, }Ec2";; - esac -fi - -db_input low cloud-init-base/datasources || true -db_go - -exit 0 From a3111f2f727b6f8bf7839368ab9c3d3fbed45116 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 22 Sep 2025 11:31:08 -0600 Subject: [PATCH 146/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index 4434aa83631..e97645ef26a 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -1,7 +1,7 @@ name: ShellCheck Debian package scripts env: - # This is a space separated string for multiple globs chad + # This is a space separated string for multiple globs # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` # Assumption in this workflow: the resolved filepaths do not contain spaces. GLOBS_TO_SHELLCHECK: "debian/cherry-pick debian/*.config debian/*.postinst debian/*.postrm debian/*.preinst debian/*.prerm packages/debian/*.postrm" From e8f253760e93fd085e880228da9896d7201e06bb Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 23 Sep 2025 17:05:25 -0600 Subject: [PATCH 147/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index e97645ef26a..6b6bdf499f8 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -15,6 +15,7 @@ on: # then the second PR will show these checks that were triggered by the first PR but not the second PR. branches: - 'ubuntu/**' + - main concurrency: group: 'ci-${{ github.workflow }}-${{ github.ref }}' From 3366c5a5b2500a2033c0e60e5430e23899cbfd72 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 23 Sep 2025 17:09:38 -0600 Subject: [PATCH 148/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index 6b6bdf499f8..a23da2eef77 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -3,6 +3,7 @@ name: ShellCheck Debian package scripts env: # This is a space separated string for multiple globs # Do not use curly braces as they will be treated as literal string in `git ls-files ${GLOBS_TO_SHELLCHECK}` + # If you decide to use globstar, make sure to use the bash shell and to `shopt -s globstar` # Assumption in this workflow: the resolved filepaths do not contain spaces. GLOBS_TO_SHELLCHECK: "debian/cherry-pick debian/*.config debian/*.postinst debian/*.postrm debian/*.preinst debian/*.prerm packages/debian/*.postrm" @@ -15,7 +16,6 @@ on: # then the second PR will show these checks that were triggered by the first PR but not the second PR. branches: - 'ubuntu/**' - - main concurrency: group: 'ci-${{ github.workflow }}-${{ github.ref }}' @@ -33,7 +33,7 @@ jobs: - name: Get all matching changed files id: matching-changed-files - # make sure to use a SHA not a version + # For security, make sure to use a SHA not a version uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 with: files: ${{ env.GLOBS_TO_SHELLCHECK }} @@ -43,19 +43,17 @@ jobs: env: ALL_CHANGED_FILES: ${{ steps.matching-changed-files.outputs.all_changed_and_modified_files }} run: | - shopt -s globstar if [ -z "${ALL_CHANGED_FILES}" ] then echo "There are no changed files in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" else - echo "The changed and modified (including deleted) files are ${ALL_CHANGED_FILES}" - echo "The globs used are ${GLOBS_TO_SHELLCHECK}" RETAINED_CHANGED_FILES=$(git ls-files ${ALL_CHANGED_FILES} | tr '\n' ' ') #filter out deleted files - echo "The matching and changed files are ${RETAINED_CHANGED_FILES}" if [ -z "${RETAINED_CHANGED_FILES}" ] then echo "There are no changed files remaining in the repo which match the glob pattern \'${GLOBS_TO_SHELLCHECK}\' so shellcheck will not run" else + echo "shellcheck will run on the remaining changed files: ${RETAINED_CHANGED_FILES}" shellcheck ${RETAINED_CHANGED_FILES} + echo "shellcheck succeeded running on the remaining changed files" fi fi From f94778ed24a0dbcfecdbaf3082d253b78d436a60 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 23 Sep 2025 17:12:47 -0600 Subject: [PATCH 149/150] Empty commit for CI trigger From 2d4ed1864494f469ff8ecc5c8a8ce33f978d749b Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 23 Sep 2025 17:14:32 -0600 Subject: [PATCH 150/150] wip --- .github/workflows/shellcheck-debian-scripts.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/shellcheck-debian-scripts.yml b/.github/workflows/shellcheck-debian-scripts.yml index a23da2eef77..442f71d5e5a 100644 --- a/.github/workflows/shellcheck-debian-scripts.yml +++ b/.github/workflows/shellcheck-debian-scripts.yml @@ -16,6 +16,7 @@ on: # then the second PR will show these checks that were triggered by the first PR but not the second PR. branches: - 'ubuntu/**' + - main concurrency: group: 'ci-${{ github.workflow }}-${{ github.ref }}'