From 19c9a22e236e853e743888f2f2ce95220f8c7714 Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Fri, 3 Jul 2026 17:01:09 +0300 Subject: [PATCH 1/2] Move `templates` and `ethernet_nmconnection_file` to base.py Signed-off-by: Mihaela Balutoiu --- coriolis/osmorphing/base.py | 108 +++++++++++++++ coriolis/osmorphing/redhat.py | 102 +------------- coriolis/tests/osmorphing/test_base.py | 163 +++++++++++++++++++++++ coriolis/tests/osmorphing/test_redhat.py | 56 ++++---- 4 files changed, 300 insertions(+), 129 deletions(-) diff --git a/coriolis/osmorphing/base.py b/coriolis/osmorphing/base.py index 7e7c08cdd..f963edd76 100644 --- a/coriolis/osmorphing/base.py +++ b/coriolis/osmorphing/base.py @@ -18,6 +18,39 @@ GRUB2_SERIAL = "serial --word=8 --stop=1 --speed=%d --parity=%s --unit=0" LOG = logging.getLogger(__name__) +IFCFG_TEMPLATE = """ +TYPE=Ethernet +BOOTPROTO=dhcp +DEFROUTE=yes +IPV4_FAILURE_FATAL=no +IPV6INIT=yes +IPV6_AUTOCONF=yes +IPV6_DEFROUTE=yes +IPV6_FAILURE_FATAL=no +NAME=%(device_name)s +DEVICE=%(device_name)s +ONBOOT=yes +NM_CONTROLLED=%(nm_controlled)s +""" + +NMCONNECTION_TEMPLATE = """[connection] +id=%(device_name)s +uuid=%(connection_uuid)s +type=ethernet +interface-name=%(device_name)s +autoconnect=true + +[ethernet] + +[ipv4] +method=auto +may-fail=false + +[ipv6] +method=auto +addr-gen-mode=default +""" + # Required OS release fields which are expected from the OSDetect tools. # 'schemas.CORIOLIS_DETECTED_OS_MORPHING_INFO_SCHEMA' schema: @@ -222,6 +255,12 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools): _packages = {} _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network-scripts" _NM_CONNECTIONS_PATH = "etc/NetworkManager/system-connections" + # ifcfg profile template written for DHCP NICs. Subclasses may override it + # when the target uses a different ifcfg format (e.g. wicked-based SUSE). + _IFCFG_TEMPLATE = IFCFG_TEMPLATE + # Minimum OS major version at which ifcfg profiles should be marked as + # NetworkManager-controlled (NM_CONTROLLED=yes). + _IFCFG_NM_CONTROLLED_MIN_VERSION = None def __init__(self, conn, os_root_dir, os_root_dev, hypervisor, event_manager, detected_os_info, osmorphing_parameters, @@ -467,6 +506,75 @@ def _get_keyfiles_by_type(self, nmconnection_type, network_scripts_path): keyfiles.append((file, keyfile)) return keyfiles + def _get_existing_ethernet_nmconnection_files(self): + if not self._test_path(self._NM_CONNECTIONS_PATH): + return [] + return [cfg_path for cfg_path, _ in self._get_keyfiles_by_type( + "ethernet", self._NM_CONNECTIONS_PATH)] + + def _get_ifcfg_nm_controlled(self): + min_version = self._IFCFG_NM_CONTROLLED_MIN_VERSION + if min_version is not None and self._version_supported_util( + self._version, minimum=min_version): + return "yes" + return "no" + + def _backup_nmconnection_files(self, nmconnection_files=None, + backup_file_suffix=".bak"): + if nmconnection_files is None: + nmconnection_files = ( + self._get_existing_ethernet_nmconnection_files()) + for cfg_path in nmconnection_files: + self._exec_cmd_chroot( + 'mv "%s" "%s%s"' % (cfg_path, cfg_path, backup_file_suffix)) + LOG.debug("Backed up nmconnection profile '%s'", cfg_path) + + def _backup_ethernet_ifcfg_configs(self, backup_file_suffix=".bak"): + if not self._test_path(self._NETWORK_SCRIPTS_PATH): + return + for cfg_path, _ in self._get_ifcfgs_by_type( + "Ethernet", self._NETWORK_SCRIPTS_PATH): + if os.path.basename(cfg_path) == "ifcfg-lo": + continue + self._exec_cmd_chroot( + 'mv "%s" "%s%s"' % (cfg_path, cfg_path, backup_file_suffix)) + LOG.debug("Backed up ifcfg profile '%s'", cfg_path) + + def _write_nic_configs(self, nics_info): + self._backup_ethernet_ifcfg_configs() + for idx, _ in enumerate(nics_info or []): + dev_name = "eth%d" % idx + cfg_path = "%s/ifcfg-%s" % (self._NETWORK_SCRIPTS_PATH, dev_name) + self._write_file_sudo( + cfg_path, + self._IFCFG_TEMPLATE % { + "device_name": dev_name, + "nm_controlled": self._get_ifcfg_nm_controlled(), + }) + + def _write_nmconnection_configs(self, nics_info, nmconnection_files=None): + nics_info = nics_info or [] + if not nics_info: + return + + # Systems may have both nmconnection keyfiles and legacy ifcfg + # profiles; back up Ethernet profiles from both so stale source configs + # cannot override the freshly written DHCP profiles. + self._backup_nmconnection_files(nmconnection_files) + self._backup_ethernet_ifcfg_configs() + + for idx, _ in enumerate(nics_info): + dev_name = "eth%d" % idx + cfg_path = "%s/%s.nmconnection" % ( + self._NM_CONNECTIONS_PATH, dev_name) + self._write_file_sudo( + cfg_path, + NMCONNECTION_TEMPLATE % { + "device_name": dev_name, + "connection_uuid": str(uuid.uuid4()), + }) + self._exec_cmd_chroot("chmod 600 /%s" % cfg_path) + def _copy_resolv_conf(self): resolv_conf = "etc/resolv.conf" resolv_conf_path = os.path.join(self._os_root_dir, resolv_conf) diff --git a/coriolis/osmorphing/redhat.py b/coriolis/osmorphing/redhat.py index 869a2d214..b2b08ccc6 100644 --- a/coriolis/osmorphing/redhat.py +++ b/coriolis/osmorphing/redhat.py @@ -23,43 +23,10 @@ RELEASE_FEDORA = "Fedora" -IFCFG_TEMPLATE = """ -TYPE=Ethernet -BOOTPROTO=dhcp -DEFROUTE=yes -IPV4_FAILURE_FATAL=no -IPV6INIT=yes -IPV6_AUTOCONF=yes -IPV6_DEFROUTE=yes -IPV6_FAILURE_FATAL=no -NAME=%(device_name)s -DEVICE=%(device_name)s -ONBOOT=yes -NM_CONTROLLED=%(nm_controlled)s -""" - -NMCONNECTION_TEMPLATE = """[connection] -id=%(device_name)s -uuid=%(connection_uuid)s -type=ethernet -interface-name=%(device_name)s -autoconnect=true - -[ethernet] - -[ipv4] -method=auto -may-fail=false - -[ipv6] -method=auto -addr-gen-mode=default -""" - - class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools): BIOS_GRUB_LOCATION = "/boot/grub2" UEFI_GRUB_LOCATION = "/boot/efi/EFI/redhat" + _IFCFG_NM_CONTROLLED_MIN_VERSION = 8 @classmethod def check_os_supported(cls, detected_os_info): @@ -112,11 +79,6 @@ def _has_systemd(self): except Exception: return False - def _get_ifcfg_nm_controlled(self): - if self._version_supported_util(self._version, minimum=8): - return "yes" - return "no" - def _set_dhcp_net_config(self, ifcfgs_ethernet): for ifcfg_file, iface_cfg in ifcfgs_ethernet: if iface_cfg.get("BOOTPROTO") == "none": @@ -141,68 +103,6 @@ def _set_dhcp_net_config(self, ifcfgs_ethernet): del network_cfg["GATEWAY"] self._write_config_file(network_cfg_file, network_cfg) - def _get_existing_ethernet_nmconnection_files(self): - if not self._test_path(self._NM_CONNECTIONS_PATH): - return [] - return [cfg_path for cfg_path, _ in self._get_keyfiles_by_type( - "ethernet", self._NM_CONNECTIONS_PATH)] - - def _backup_nmconnection_files(self, nmconnection_files=None, - backup_file_suffix=".bak"): - if nmconnection_files is None: - nmconnection_files = ( - self._get_existing_ethernet_nmconnection_files()) - for cfg_path in nmconnection_files: - self._exec_cmd_chroot( - 'mv "%s" "%s%s"' % (cfg_path, cfg_path, backup_file_suffix)) - LOG.debug("Backed up nmconnection profile '%s'", cfg_path) - - def _backup_all_ifcfg_configs(self, backup_file_suffix=".bak"): - if not self._test_path(self._NETWORK_SCRIPTS_PATH): - return - for cfg_path, _ in self._get_ifcfgs_by_type( - "Ethernet", self._NETWORK_SCRIPTS_PATH): - if os.path.basename(cfg_path) == "ifcfg-lo": - continue - self._exec_cmd_chroot( - 'mv "%s" "%s%s"' % (cfg_path, cfg_path, backup_file_suffix)) - LOG.debug("Backed up ifcfg profile '%s'", cfg_path) - - def _write_nic_configs(self, nics_info): - self._backup_all_ifcfg_configs() - for idx, _ in enumerate(nics_info or []): - dev_name = "eth%d" % idx - cfg_path = "%s/ifcfg-%s" % (self._NETWORK_SCRIPTS_PATH, dev_name) - self._write_file_sudo( - cfg_path, - IFCFG_TEMPLATE % { - "device_name": dev_name, - "nm_controlled": self._get_ifcfg_nm_controlled(), - }) - - def _write_nmconnection_configs(self, nics_info, nmconnection_files): - nics_info = nics_info or [] - if not nics_info: - return - - # Red Hat-based systems may have both nmconnection keyfiles and legacy - # ifcfg profiles; back up Ethernet profiles from both so stale source - # configs cannot override the freshly written DHCP profiles. - self._backup_nmconnection_files(nmconnection_files) - self._backup_all_ifcfg_configs() - - for idx, _ in enumerate(nics_info): - dev_name = "eth%d" % idx - cfg_path = "%s/%s.nmconnection" % ( - self._NM_CONNECTIONS_PATH, dev_name) - self._write_file_sudo( - cfg_path, - NMCONNECTION_TEMPLATE % { - "device_name": dev_name, - "connection_uuid": str(uuid.uuid4()), - }) - self._exec_cmd_chroot("chmod 600 /%s" % cfg_path) - def _comment_keys_from_ifcfg_files( self, keys, interfaces=None, backup_file_suffix=".bak"): """ Comments the provided list of keys from all 'ifcfg-*' files. diff --git a/coriolis/tests/osmorphing/test_base.py b/coriolis/tests/osmorphing/test_base.py index e4ff65e9b..bf477a861 100644 --- a/coriolis/tests/osmorphing/test_base.py +++ b/coriolis/tests/osmorphing/test_base.py @@ -588,6 +588,169 @@ def test__get_keyfiles_by_type(self, mock_get_nmconnection_files, self.assertEqual( result, [(mock.sentinel.nmconn_file, {"type": "ethernet"})]) + def test__get_ifcfg_nm_controlled_default(self): + # No minimum version configured -> never NetworkManager-controlled. + self.assertIsNone( + self.os_morphing_tools._IFCFG_NM_CONTROLLED_MIN_VERSION) + + self.assertEqual( + "no", self.os_morphing_tools._get_ifcfg_nm_controlled()) + + def test__get_ifcfg_nm_controlled_below_minimum(self): + self.os_morphing_tools._IFCFG_NM_CONTROLLED_MIN_VERSION = 8 + self.os_morphing_tools._version = "7" + + self.assertEqual( + "no", self.os_morphing_tools._get_ifcfg_nm_controlled()) + + def test__get_ifcfg_nm_controlled_at_or_above_minimum(self): + self.os_morphing_tools._IFCFG_NM_CONTROLLED_MIN_VERSION = 8 + self.os_morphing_tools._version = "9" + + self.assertEqual( + "yes", self.os_morphing_tools._get_ifcfg_nm_controlled()) + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + def test__backup_nmconnection_files_explicit_list( + self, mock_exec_cmd_chroot): + nmconnection_files = [ + 'etc/NetworkManager/system-connections/eth0.nmconnection', + 'etc/NetworkManager/system-connections/eth1.nmconnection'] + + with self.assertLogs('coriolis.osmorphing.base', level=logging.DEBUG): + self.os_morphing_tools._backup_nmconnection_files( + nmconnection_files) + + mock_exec_cmd_chroot.assert_has_calls([ + mock.call( + 'mv "etc/NetworkManager/system-connections/eth0.nmconnection" ' + '"etc/NetworkManager/system-connections/eth0.nmconnection.bak"' + ), + mock.call( + 'mv "etc/NetworkManager/system-connections/eth1.nmconnection" ' + '"etc/NetworkManager/system-connections/eth1.nmconnection.bak"' + ), + ]) + + @mock.patch.object( + base.BaseLinuxOSMorphingTools, + '_get_existing_ethernet_nmconnection_files') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + def test__backup_nmconnection_files_fetches_files( + self, mock_exec_cmd_chroot, + mock_get_existing_ethernet_nmconnection_files): + mock_get_existing_ethernet_nmconnection_files.return_value = [ + 'etc/NetworkManager/system-connections/eth0.nmconnection'] + + with self.assertLogs('coriolis.osmorphing.base', level=logging.DEBUG): + self.os_morphing_tools._backup_nmconnection_files() + + mock_get_existing_ethernet_nmconnection_files.assert_called_once_with() + mock_exec_cmd_chroot.assert_called_once_with( + 'mv "etc/NetworkManager/system-connections/eth0.nmconnection" ' + '"etc/NetworkManager/system-connections/eth0.nmconnection.bak"') + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_get_ifcfgs_by_type') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__backup_ethernet_ifcfg_configs( + self, mock_test_path, mock_exec_cmd_chroot, + mock_get_ifcfgs_by_type): + mock_test_path.return_value = True + mock_get_ifcfgs_by_type.return_value = [ + ("etc/sysconfig/network-scripts/ifcfg-eth0", {}), + # ifcfg-lo must be skipped. + ("etc/sysconfig/network-scripts/ifcfg-lo", {}), + ] + + with self.assertLogs('coriolis.osmorphing.base', level=logging.DEBUG): + self.os_morphing_tools._backup_ethernet_ifcfg_configs() + + mock_exec_cmd_chroot.assert_called_once_with( + 'mv "etc/sysconfig/network-scripts/ifcfg-eth0" ' + '"etc/sysconfig/network-scripts/ifcfg-eth0.bak"') + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_get_ifcfgs_by_type') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__backup_ethernet_ifcfg_configs_no_dir( + self, mock_test_path, mock_exec_cmd_chroot, + mock_get_ifcfgs_by_type): + mock_test_path.return_value = False + + self.os_morphing_tools._backup_ethernet_ifcfg_configs() + + mock_get_ifcfgs_by_type.assert_not_called() + mock_exec_cmd_chroot.assert_not_called() + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_ethernet_ifcfg_configs') + def test__write_nic_configs( + self, mock_backup_ethernet_ifcfg_configs, mock_write_file_sudo): + nics_info = [{'name': 'eth0'}, {'name': 'eth1'}] + + self.os_morphing_tools._write_nic_configs(nics_info) + + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() + mock_write_file_sudo.assert_has_calls([ + mock.call( + "etc/sysconfig/network-scripts/ifcfg-eth0", + base.IFCFG_TEMPLATE % { + "device_name": "eth0", + "nm_controlled": "no", + }), + mock.call( + "etc/sysconfig/network-scripts/ifcfg-eth1", + base.IFCFG_TEMPLATE % { + "device_name": "eth1", + "nm_controlled": "no", + }), + ]) + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_ethernet_ifcfg_configs') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_nmconnection_files') + def test__write_nmconnection_configs( + self, mock_backup_nmconnection_files, + mock_backup_ethernet_ifcfg_configs, mock_write_file_sudo, + mock_exec_cmd_chroot): + nics_info = [{'name': 'eth0'}] + nmconnection_files = [ + 'etc/NetworkManager/system-connections/eth0.nmconnection'] + + self.os_morphing_tools._write_nmconnection_configs( + nics_info, nmconnection_files) + + mock_backup_nmconnection_files.assert_called_once_with( + nmconnection_files) + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() + mock_write_file_sudo.assert_called_once() + args, _ = mock_write_file_sudo.call_args + self.assertEqual( + args[0], + "etc/NetworkManager/system-connections/eth0.nmconnection") + self.assertIn("[connection]", args[1]) + self.assertIn("interface-name=eth0", args[1]) + mock_exec_cmd_chroot.assert_called_once_with( + "chmod 600 /etc/NetworkManager/system-connections/" + "eth0.nmconnection") + + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_nmconnection_files') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_ethernet_ifcfg_configs') + def test__write_nmconnection_configs_no_nics( + self, mock_backup_ethernet_ifcfg_configs, + mock_backup_nmconnection_files): + self.os_morphing_tools._write_nmconnection_configs(None, None) + + mock_backup_nmconnection_files.assert_not_called() + mock_backup_ethernet_ifcfg_configs.assert_not_called() + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd') def test__copy_resolv_conf(self, mock_exec_cmd, mock_test_path): diff --git a/coriolis/tests/osmorphing/test_redhat.py b/coriolis/tests/osmorphing/test_redhat.py index 8d9d054a7..761d07252 100644 --- a/coriolis/tests/osmorphing/test_redhat.py +++ b/coriolis/tests/osmorphing/test_redhat.py @@ -150,26 +150,26 @@ def test__set_dhcp_net_config(self, mock_read_config_file, ]) @mock.patch.object( - redhat.BaseRedHatMorphingTools, '_backup_all_ifcfg_configs') + redhat.BaseRedHatMorphingTools, '_backup_ethernet_ifcfg_configs') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') def test_write_nic_configs( - self, mock_write_file_sudo, mock_backup_all_ifcfg_configs): + self, mock_write_file_sudo, mock_backup_ethernet_ifcfg_configs): nics_info = [{'name': 'eth0'}, {'name': 'eth1'}] self.morphing_tools._write_nic_configs(nics_info) - mock_backup_all_ifcfg_configs.assert_called_once_with() + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() mock_write_file_sudo.assert_has_calls([ mock.call( "etc/sysconfig/network-scripts/ifcfg-eth0", - redhat.IFCFG_TEMPLATE % { + base.IFCFG_TEMPLATE % { "device_name": "eth0", "nm_controlled": "no", }, ), mock.call( "etc/sysconfig/network-scripts/ifcfg-eth1", - redhat.IFCFG_TEMPLATE % { + base.IFCFG_TEMPLATE % { "device_name": "eth1", "nm_controlled": "no", }, @@ -177,19 +177,19 @@ def test_write_nic_configs( ]) @mock.patch.object( - redhat.BaseRedHatMorphingTools, '_backup_all_ifcfg_configs') + redhat.BaseRedHatMorphingTools, '_backup_ethernet_ifcfg_configs') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') def test_write_nic_configs_rhel8( - self, mock_write_file_sudo, mock_backup_all_ifcfg_configs): + self, mock_write_file_sudo, mock_backup_ethernet_ifcfg_configs): self.morphing_tools._version = '8.10' nics_info = [{'name': 'eth0'}] self.morphing_tools._write_nic_configs(nics_info) - mock_backup_all_ifcfg_configs.assert_called_once_with() + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() mock_write_file_sudo.assert_called_once_with( "etc/sysconfig/network-scripts/ifcfg-eth0", - redhat.IFCFG_TEMPLATE % { + base.IFCFG_TEMPLATE % { "device_name": "eth0", "nm_controlled": "yes", }, @@ -205,7 +205,7 @@ def test__get_ifcfg_nm_controlled(self, release_version, expected): self.assertEqual(expected, result) @mock.patch.object( - redhat.BaseRedHatMorphingTools, '_backup_all_ifcfg_configs' + redhat.BaseRedHatMorphingTools, '_backup_ethernet_ifcfg_configs' ) @mock.patch.object( redhat.BaseRedHatMorphingTools, '_backup_nmconnection_files' @@ -215,7 +215,7 @@ def test__get_ifcfg_nm_controlled(self, release_version, expected): def test_write_nmconnection_configs( self, mock_exec_cmd_chroot, mock_write_file_sudo, mock_backup_nmconnection_files, - mock_backup_all_ifcfg_configs): + mock_backup_ethernet_ifcfg_configs): nics_info = [{'name': 'eth0'}] nmconnection_files = [ 'etc/NetworkManager/system-connections/eth0.nmconnection'] @@ -225,7 +225,7 @@ def test_write_nmconnection_configs( mock_backup_nmconnection_files.assert_called_once_with( nmconnection_files) - mock_backup_all_ifcfg_configs.assert_called_once_with() + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() mock_write_file_sudo.assert_called_once() args, _ = mock_write_file_sudo.call_args self.assertEqual( @@ -241,7 +241,7 @@ def test_write_nmconnection_configs( ) @mock.patch.object( - redhat.BaseRedHatMorphingTools, '_backup_all_ifcfg_configs' + redhat.BaseRedHatMorphingTools, '_backup_ethernet_ifcfg_configs' ) @mock.patch.object( redhat.BaseRedHatMorphingTools, '_backup_nmconnection_files' @@ -251,12 +251,12 @@ def test_write_nmconnection_configs( def test_write_nmconnection_configs_no_nics( self, mock_exec_cmd_chroot, mock_write_file_sudo, mock_backup_nmconnection_files, - mock_backup_all_ifcfg_configs): + mock_backup_ethernet_ifcfg_configs): self.morphing_tools._write_nmconnection_configs( None, ['etc/NetworkManager/system-connections/eth0.nmconnection']) mock_backup_nmconnection_files.assert_not_called() - mock_backup_all_ifcfg_configs.assert_not_called() + mock_backup_ethernet_ifcfg_configs.assert_not_called() mock_write_file_sudo.assert_not_called() mock_exec_cmd_chroot.assert_not_called() @@ -276,7 +276,7 @@ def test__backup_nmconnection_files(self, mock_exec_cmd_chroot): ) @mock.patch.object( - redhat.BaseRedHatMorphingTools, + base.BaseLinuxOSMorphingTools, '_get_existing_ethernet_nmconnection_files') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') def test__backup_nmconnection_files_fetches_files( @@ -291,7 +291,7 @@ def test__backup_nmconnection_files_fetches_files( mock_exec_cmd_chroot.assert_called_once() @mock.patch.object( - redhat.BaseRedHatMorphingTools, + base.BaseLinuxOSMorphingTools, '_get_existing_ethernet_nmconnection_files') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') def test__backup_nmconnection_files_no_files( @@ -306,9 +306,9 @@ def test__backup_nmconnection_files_no_files( @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_get_ifcfgs_by_type') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') - def test__backup_all_ifcfg_configs(self, mock_test_path, - mock_get_ifcfgs_by_type, - mock_exec_cmd_chroot): + def test__backup_ethernet_ifcfg_configs(self, mock_test_path, + mock_get_ifcfgs_by_type, + mock_exec_cmd_chroot): mock_test_path.return_value = True mock_get_ifcfgs_by_type.return_value = [ ("etc/sysconfig/network-scripts/ifcfg-ens33", @@ -317,7 +317,7 @@ def test__backup_all_ifcfg_configs(self, mock_test_path, {"TYPE": "Ethernet"}), ] - self.morphing_tools._backup_all_ifcfg_configs() + self.morphing_tools._backup_ethernet_ifcfg_configs() mock_get_ifcfgs_by_type.assert_called_once_with( "Ethernet", self.morphing_tools._NETWORK_SCRIPTS_PATH) @@ -336,12 +336,12 @@ def test__backup_all_ifcfg_configs(self, mock_test_path, @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_get_ifcfgs_by_type') @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') - def test__backup_all_ifcfg_configs_no_dir(self, mock_test_path, - mock_get_ifcfgs_by_type, - mock_exec_cmd_chroot): + def test__backup_ethernet_ifcfg_configs_no_dir(self, mock_test_path, + mock_get_ifcfgs_by_type, + mock_exec_cmd_chroot): mock_test_path.return_value = False - self.morphing_tools._backup_all_ifcfg_configs() + self.morphing_tools._backup_ethernet_ifcfg_configs() mock_get_ifcfgs_by_type.assert_not_called() mock_exec_cmd_chroot.assert_not_called() @@ -383,7 +383,7 @@ def test__get_existing_ethernet_nmconnection_files_no_dir( redhat.BaseRedHatMorphingTools, '_write_nmconnection_configs' ) @mock.patch.object( - redhat.BaseRedHatMorphingTools, + base.BaseLinuxOSMorphingTools, '_get_existing_ethernet_nmconnection_files', ) def test_set_net_config_dhcp( @@ -412,7 +412,7 @@ def test_set_net_config_dhcp( ) @mock.patch.object(redhat.BaseRedHatMorphingTools, '_write_nic_configs') @mock.patch.object( - redhat.BaseRedHatMorphingTools, + base.BaseLinuxOSMorphingTools, '_get_existing_ethernet_nmconnection_files', ) def test_set_net_config_dhcp_nmconnection( @@ -443,7 +443,7 @@ def test_set_net_config_dhcp_nmconnection( ) @mock.patch.object(redhat.BaseRedHatMorphingTools, '_write_nic_configs') @mock.patch.object( - redhat.BaseRedHatMorphingTools, + base.BaseLinuxOSMorphingTools, '_get_existing_ethernet_nmconnection_files', ) def test_set_net_config_dhcp_nmconnection_no_nics( From 0458a90f8a700e2bc68ab9ae13523aace81bc880 Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Fri, 3 Jul 2026 17:01:09 +0300 Subject: [PATCH 2/2] Add suse network config support Signed-off-by: Mihaela Balutoiu --- coriolis/osmorphing/suse.py | 50 ++++++- coriolis/tests/osmorphing/test_suse.py | 197 +++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 4 deletions(-) diff --git a/coriolis/osmorphing/suse.py b/coriolis/osmorphing/suse.py index 7aaf755d1..9dd4125dc 100644 --- a/coriolis/osmorphing/suse.py +++ b/coriolis/osmorphing/suse.py @@ -26,11 +26,20 @@ "https://download.opensuse.org/repositories/Cloud:/Tools/%s/") CLOUD_TOOLS_NEW_URL_MINIMUM_VERSION = 16 +# SUSE (SLES/openSUSE 12 and 15) manages the network with wicked, which reads +# ifcfg files. SLES 16 switched to NetworkManager keyfiles and is handled by +# the inherited '_write_nmconnection_configs'. +SUSE_IFCFG_TEMPLATE = """BOOTPROTO='dhcp' +STARTMODE='auto' +""" + class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools): BIOS_GRUB_LOCATION = "/boot/grub2" UEFI_GRUB_LOCATION = "/boot/efi/EFI/suse" + _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network" + _IFCFG_TEMPLATE = SUSE_IFCFG_TEMPLATE @classmethod def get_required_detected_os_info_fields(cls): @@ -61,12 +70,45 @@ def check_os_supported(cls, detected_os_info): return False def disable_predictable_nic_names(self): - # TODO(gsamfira): implement once we have networking support - pass + grub_cfg = "etc/default/grub" + if not self._test_path(grub_cfg): + LOG.warning( + "Could not find /%s. Skipping predictable NIC names " + "disabling.", grub_cfg) + return + contents = self._read_file_sudo(grub_cfg) + cfg = utils.Grub2ConfigEditor(contents) + cfg.append_to_option( + "GRUB_CMDLINE_LINUX_DEFAULT", + {"opt_type": "key_val", "opt_key": "net.ifnames", "opt_val": 0}) + cfg.append_to_option( + "GRUB_CMDLINE_LINUX_DEFAULT", + {"opt_type": "key_val", "opt_key": "biosdevname", "opt_val": 0}) + cfg.append_to_option( + "GRUB_CMDLINE_LINUX", + {"opt_type": "key_val", "opt_key": "net.ifnames", "opt_val": 0}) + cfg.append_to_option( + "GRUB_CMDLINE_LINUX", + {"opt_type": "key_val", "opt_key": "biosdevname", "opt_val": 0}) + self._write_file_sudo("etc/default/grub", cfg.dump()) + self._schedule_grub2_update() def set_net_config(self, nics_info, dhcp): - # TODO(alexpilotti): add networking support - pass + if dhcp: + nics_info = nics_info or [] + if not nics_info: + return + self.disable_predictable_nic_names() + nmconnection_files = ( + self._get_existing_ethernet_nmconnection_files()) + if nmconnection_files: + self._write_nmconnection_configs(nics_info, nmconnection_files) + else: + self._write_nic_configs(nics_info) + return + + LOG.info("Setting static IP configuration") + self._setup_network_preservation(nics_info) def get_installed_packages(self): cmd = 'rpm -qa --qf "%{NAME}\\n"' diff --git a/coriolis/tests/osmorphing/test_suse.py b/coriolis/tests/osmorphing/test_suse.py index b2372b161..fe0006c71 100644 --- a/coriolis/tests/osmorphing/test_suse.py +++ b/coriolis/tests/osmorphing/test_suse.py @@ -434,3 +434,200 @@ def test_pre_packages_install_no_packages( mock_super_pre.assert_called_once_with([]) mock_enable_sles_module.assert_not_called() mock_add_cloud_tools_repo.assert_not_called() + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_get_keyfiles_by_type') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__get_existing_ethernet_nmconnection_files( + self, mock_test_path, mock_get_keyfiles_by_type): + mock_test_path.return_value = True + mock_get_keyfiles_by_type.return_value = [ + ('etc/NetworkManager/system-connections/eth0.nmconnection', {}), + ('etc/NetworkManager/system-connections/eth1.nmconnection', {})] + + result = ( + self.morphing_tools._get_existing_ethernet_nmconnection_files()) + + self.assertEqual(result, [ + 'etc/NetworkManager/system-connections/eth0.nmconnection', + 'etc/NetworkManager/system-connections/eth1.nmconnection']) + mock_get_keyfiles_by_type.assert_called_once_with( + "ethernet", "etc/NetworkManager/system-connections") + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_get_keyfiles_by_type') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__get_existing_ethernet_nmconnection_files_no_path( + self, mock_test_path, mock_get_keyfiles_by_type): + mock_test_path.return_value = False + + result = ( + self.morphing_tools._get_existing_ethernet_nmconnection_files()) + + self.assertEqual(result, []) + mock_get_keyfiles_by_type.assert_not_called() + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object(suse.BaseSUSEMorphingTools, '_schedule_grub2_update') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_read_file_sudo') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test_disable_predictable_nic_names( + self, mock_test_path, mock_read_file_sudo, + mock_schedule_grub2_update, mock_write_file_sudo): + mock_test_path.return_value = True + mock_read_file_sudo.return_value = ( + 'GRUB_CMDLINE_LINUX_DEFAULT=""\nGRUB_CMDLINE_LINUX=""\n') + + self.morphing_tools.disable_predictable_nic_names() + + mock_read_file_sudo.assert_called_once_with("etc/default/grub") + mock_write_file_sudo.assert_called_once() + written_path, written_contents = mock_write_file_sudo.call_args[0] + self.assertEqual("etc/default/grub", written_path) + self.assertIn("net.ifnames=0", written_contents) + self.assertIn("biosdevname=0", written_contents) + # The (slow) grub regeneration must be deferred, not run eagerly. + mock_schedule_grub2_update.assert_called_once_with() + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test_disable_predictable_nic_names_no_grub_cfg(self, mock_test_path): + mock_test_path.return_value = False + + with self.assertLogs( + 'coriolis.osmorphing.suse', level=logging.WARNING): + self.morphing_tools.disable_predictable_nic_names() + + def test__ifcfg_class_attributes(self): + self.assertEqual( + "etc/sysconfig/network", + self.morphing_tools._NETWORK_SCRIPTS_PATH) + self.assertEqual( + suse.SUSE_IFCFG_TEMPLATE, self.morphing_tools._IFCFG_TEMPLATE) + self.assertNotIn("NM_CONTROLLED", suse.SUSE_IFCFG_TEMPLATE) + self.assertIn("BOOTPROTO='dhcp'", suse.SUSE_IFCFG_TEMPLATE) + self.assertIn("STARTMODE='auto'", suse.SUSE_IFCFG_TEMPLATE) + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_ethernet_ifcfg_configs') + def test__write_nic_configs( + self, mock_backup_ethernet_ifcfg_configs, mock_write_file_sudo): + nics_info = [{'name': 'eth0'}, {'name': 'eth1'}] + + self.morphing_tools._write_nic_configs(nics_info) + + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() + mock_write_file_sudo.assert_has_calls([ + mock.call( + "etc/sysconfig/network/ifcfg-eth0", suse.SUSE_IFCFG_TEMPLATE), + mock.call( + "etc/sysconfig/network/ifcfg-eth1", suse.SUSE_IFCFG_TEMPLATE), + ]) + + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_ethernet_ifcfg_configs') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_backup_nmconnection_files') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + def test__write_nmconnection_configs( + self, mock_exec_cmd_chroot, mock_write_file_sudo, + mock_backup_nmconnection_files, + mock_backup_ethernet_ifcfg_configs): + nics_info = [{'name': 'eth0'}] + nmconnection_files = [ + 'etc/NetworkManager/system-connections/eth0.nmconnection'] + + self.morphing_tools._write_nmconnection_configs( + nics_info, nmconnection_files) + + mock_backup_nmconnection_files.assert_called_once_with( + nmconnection_files) + mock_backup_ethernet_ifcfg_configs.assert_called_once_with() + mock_write_file_sudo.assert_called_once() + args, _ = mock_write_file_sudo.call_args + self.assertEqual( + args[0], + "etc/NetworkManager/system-connections/eth0.nmconnection") + self.assertIn("[connection]", args[1]) + self.assertIn("interface-name=eth0", args[1]) + self.assertIn("method=auto", args[1]) + self.assertIn("may-fail=false", args[1]) + mock_exec_cmd_chroot.assert_called_once_with( + "chmod 600 /etc/NetworkManager/system-connections/" + "eth0.nmconnection") + + @mock.patch.object( + suse.BaseSUSEMorphingTools, 'disable_predictable_nic_names') + @mock.patch.object(suse.BaseSUSEMorphingTools, '_write_nic_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_write_nmconnection_configs') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, + '_get_existing_ethernet_nmconnection_files') + def test_set_net_config_dhcp( + self, mock_get_existing_ethernet_nmconnection_files, + mock_write_nmconnection_configs, + mock_write_nic_configs, + mock_disable_predictable_nic_names): + mock_get_existing_ethernet_nmconnection_files.return_value = [] + nics_info = [{'name': 'eth0'}] + + self.morphing_tools.set_net_config(nics_info, dhcp=True) + + mock_get_existing_ethernet_nmconnection_files.assert_called_once_with() + mock_write_nmconnection_configs.assert_not_called() + mock_disable_predictable_nic_names.assert_called_once() + mock_write_nic_configs.assert_called_once_with(nics_info) + + @mock.patch.object( + suse.BaseSUSEMorphingTools, 'disable_predictable_nic_names') + @mock.patch.object(suse.BaseSUSEMorphingTools, '_write_nic_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_write_nmconnection_configs') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, + '_get_existing_ethernet_nmconnection_files') + def test_set_net_config_dhcp_nmconnection( + self, mock_get_existing_ethernet_nmconnection_files, + mock_write_nmconnection_configs, + mock_write_nic_configs, + mock_disable_predictable_nic_names): + nm_files = [ + 'etc/NetworkManager/system-connections/eth0.nmconnection'] + mock_get_existing_ethernet_nmconnection_files.return_value = nm_files + nics_info = [{'name': 'eth0'}] + + self.morphing_tools.set_net_config(nics_info, dhcp=True) + + mock_disable_predictable_nic_names.assert_called_once() + mock_write_nmconnection_configs.assert_called_once_with( + nics_info, nm_files) + mock_write_nic_configs.assert_not_called() + + @mock.patch.object( + suse.BaseSUSEMorphingTools, 'disable_predictable_nic_names') + @mock.patch.object(suse.BaseSUSEMorphingTools, '_write_nic_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_write_nmconnection_configs') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, + '_get_existing_ethernet_nmconnection_files') + def test_set_net_config_dhcp_no_nics( + self, mock_get_existing_ethernet_nmconnection_files, + mock_write_nmconnection_configs, + mock_write_nic_configs, + mock_disable_predictable_nic_names): + self.morphing_tools.set_net_config(None, dhcp=True) + + mock_get_existing_ethernet_nmconnection_files.assert_not_called() + mock_disable_predictable_nic_names.assert_not_called() + mock_write_nmconnection_configs.assert_not_called() + mock_write_nic_configs.assert_not_called() + + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_setup_network_preservation') + def test_set_net_config_static(self, mock_setup_network_preservation): + nics_info = [{'name': 'eth0'}] + + self.morphing_tools.set_net_config(nics_info, dhcp=False) + + mock_setup_network_preservation.assert_called_once_with(nics_info)