diff --git a/coriolis/osmorphing/suse.py b/coriolis/osmorphing/suse.py index 7aaf755d..488e2a74 100644 --- a/coriolis/osmorphing/suse.py +++ b/coriolis/osmorphing/suse.py @@ -10,9 +10,15 @@ from coriolis import exception from coriolis.osmorphing import base +from coriolis.osmorphing.netpreserver import ifcfg +from coriolis.osmorphing.netpreserver import nmconnection from coriolis.osmorphing.osdetect import suse as suse_detect +from coriolis.osmorphing import redhat as redhat_osmorphing from coriolis import utils +IFCFG_TEMPLATE = redhat_osmorphing.IFCFG_TEMPLATE +NMCONNECTION_TEMPLATE = redhat_osmorphing.NMCONNECTION_TEMPLATE + LOG = logging.getLogger(__name__) DETECTED_SUSE_RELEASE_FIELD_NAME = suse_detect.DETECTED_SUSE_RELEASE_FIELD_NAME @@ -29,6 +35,8 @@ class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools): + _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network-scripts" + _NM_CONNECTIONS_PATH = "etc/NetworkManager/system-connections" BIOS_GRUB_LOCATION = "/boot/grub2" UEFI_GRUB_LOCATION = "/boot/efi/EFI/suse" @@ -61,12 +69,91 @@ 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._execute_update_grub() + + def _get_nmconnection_net_preserver(self): + return nmconnection.NmconnectionNetPreserver(self) + + def _get_ifcfg_net_preserver(self): + return ifcfg.IfcfgNetPreserver(self) + + def _get_ifcfg_nm_controlled(self): + if self._version_supported_util(self._version, minimum=15): + return "yes" + return "no" + + def _write_nic_configs(self, nics_info): + for idx, _ in enumerate(nics_info): + dev_name = "eth%d" % idx + cfg_path = "%s/ifcfg-%s" % (self._NETWORK_SCRIPTS_PATH, dev_name) + if self._test_path(cfg_path): + self._exec_cmd_chroot( + "cp %s %s.bak" % (cfg_path, cfg_path) + ) + 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): + nm_net_preserver = self._get_nmconnection_net_preserver() + nm_net_preserver.backup_nmconnection_files() + device_names = ["eth%d" % idx for idx, _ in enumerate(nics_info)] + self._get_ifcfg_net_preserver().backup_ifcfg_configs(device_names) + + 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 _write_dhcp_net_config(self, nics_info): + self.disable_predictable_nic_names() + ethernet_keyfiles = None + if self._test_path(self._NM_CONNECTIONS_PATH): + ethernet_keyfiles = ( + self._get_nmconnection_net_preserver().get_ethernet_keyfiles()) + if ethernet_keyfiles: + self._write_nmconnection_configs(nics_info) + else: + self._write_nic_configs(nics_info) def set_net_config(self, nics_info, dhcp): - # TODO(alexpilotti): add networking support - pass + if dhcp: + self._write_dhcp_net_config(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 b2372b16..eb2551b3 100644 --- a/coriolis/tests/osmorphing/test_suse.py +++ b/coriolis/tests/osmorphing/test_suse.py @@ -6,6 +6,8 @@ from coriolis import exception from coriolis.osmorphing import base +from coriolis.osmorphing.netpreserver import ifcfg +from coriolis.osmorphing.netpreserver import nmconnection from coriolis.osmorphing import suse from coriolis.tests import test_base @@ -434,3 +436,210 @@ 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() + + def test__get_nmconnection_net_preserver(self): + result = self.morphing_tools._get_nmconnection_net_preserver() + + self.assertIsInstance(result, nmconnection.NmconnectionNetPreserver) + + def test__get_ifcfg_net_preserver(self): + result = self.morphing_tools._get_ifcfg_net_preserver() + + self.assertIsInstance(result, ifcfg.IfcfgNetPreserver) + + def test__get_ifcfg_nm_controlled_old_version(self): + result = self.morphing_tools._get_ifcfg_nm_controlled() + + self.assertEqual("no", result) + + def test__get_ifcfg_nm_controlled_sles15(self): + self.morphing_tools._version = "15" + + result = self.morphing_tools._get_ifcfg_nm_controlled() + + self.assertEqual("yes", result) + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__write_nic_configs_with_existing_file( + self, mock_test_path, mock_exec_cmd_chroot, mock_write_file_sudo): + nics_info = [{'name': 'eth0'}, {'name': 'eth1'}] + mock_test_path.return_value = True + + self.morphing_tools._write_nic_configs(nics_info) + + mock_exec_cmd_chroot.assert_has_calls([ + mock.call("cp etc/sysconfig/network-scripts/ifcfg-eth0 " + "etc/sysconfig/network-scripts/ifcfg-eth0.bak"), + mock.call("cp etc/sysconfig/network-scripts/ifcfg-eth1 " + "etc/sysconfig/network-scripts/ifcfg-eth1.bak"), + ]) + mock_write_file_sudo.assert_has_calls([ + mock.call( + "etc/sysconfig/network-scripts/ifcfg-eth0", + suse.IFCFG_TEMPLATE % { + "device_name": "eth0", + "nm_controlled": "no", + }, + ), + mock.call( + "etc/sysconfig/network-scripts/ifcfg-eth1", + suse.IFCFG_TEMPLATE % { + "device_name": "eth1", + "nm_controlled": "no", + }, + ), + ]) + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_write_file_sudo') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__write_nic_configs_sles15_no_existing_file( + self, mock_test_path, mock_exec_cmd_chroot, mock_write_file_sudo): + self.morphing_tools._version = "15" + nics_info = [{'name': 'eth0'}] + mock_test_path.return_value = False + + self.morphing_tools._write_nic_configs(nics_info) + + mock_exec_cmd_chroot.assert_not_called() + mock_write_file_sudo.assert_called_once_with( + "etc/sysconfig/network-scripts/ifcfg-eth0", + suse.IFCFG_TEMPLATE % { + "device_name": "eth0", + "nm_controlled": "yes", + }, + ) + + @mock.patch.object( + ifcfg.IfcfgNetPreserver, 'backup_ifcfg_configs' + ) + @mock.patch.object( + nmconnection.NmconnectionNetPreserver, 'backup_nmconnection_files' + ) + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_get_ifcfg_net_preserver' + ) + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_get_nmconnection_net_preserver' + ) + @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_get_nmconnection_net_preserver, + mock_get_ifcfg_net_preserver, + mock_backup_nmconnection_files, + mock_backup_ifcfg_configs): + mock_nm_preserver = mock_get_nmconnection_net_preserver.return_value + mock_ifcfg_preserver = mock_get_ifcfg_net_preserver.return_value + nics_info = [{'name': 'eth0'}] + + self.morphing_tools._write_nmconnection_configs(nics_info) + + mock_nm_preserver.backup_nmconnection_files.assert_called_once_with() + mock_ifcfg_preserver.backup_ifcfg_configs.assert_called_once_with( + ['eth0']) + 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, '_write_nic_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_write_nmconnection_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_get_nmconnection_net_preserver') + @mock.patch.object( + suse.BaseSUSEMorphingTools, 'disable_predictable_nic_names') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__write_dhcp_net_config_no_nm_path( + self, mock_test_path, mock_disable_predictable_nic_names, + mock_get_nm_preserver, mock_write_nmconnection_configs, + mock_write_nic_configs): + mock_test_path.return_value = False + nics_info = [{'name': 'eth0'}] + + self.morphing_tools._write_dhcp_net_config(nics_info) + + mock_disable_predictable_nic_names.assert_called_once() + mock_test_path.assert_called_once_with( + "etc/NetworkManager/system-connections") + mock_get_nm_preserver.assert_not_called() + mock_write_nic_configs.assert_called_once_with(nics_info) + mock_write_nmconnection_configs.assert_not_called() + + @mock.patch.object(suse.BaseSUSEMorphingTools, '_write_nic_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_write_nmconnection_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_get_nmconnection_net_preserver') + @mock.patch.object( + suse.BaseSUSEMorphingTools, 'disable_predictable_nic_names') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__write_dhcp_net_config_no_ethernet_keyfiles( + self, mock_test_path, mock_disable_predictable_nic_names, + mock_get_nm_preserver, mock_write_nmconnection_configs, + mock_write_nic_configs): + mock_test_path.return_value = True + mock_nm_preserver = mock_get_nm_preserver.return_value + mock_nm_preserver.get_ethernet_keyfiles.return_value = [] + nics_info = [{'name': 'eth0'}] + + self.morphing_tools._write_dhcp_net_config(nics_info) + + mock_disable_predictable_nic_names.assert_called_once() + mock_nm_preserver.get_ethernet_keyfiles.assert_called_once_with() + mock_write_nic_configs.assert_called_once_with(nics_info) + mock_write_nmconnection_configs.assert_not_called() + + @mock.patch.object(suse.BaseSUSEMorphingTools, '_write_nic_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_write_nmconnection_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_get_nmconnection_net_preserver') + @mock.patch.object( + suse.BaseSUSEMorphingTools, 'disable_predictable_nic_names') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__write_dhcp_net_config_with_ethernet_keyfiles( + self, mock_test_path, mock_disable_predictable_nic_names, + mock_get_nm_preserver, mock_write_nmconnection_configs, + mock_write_nic_configs): + mock_test_path.return_value = True + mock_nm_preserver = mock_get_nm_preserver.return_value + mock_nm_preserver.get_ethernet_keyfiles.return_value = [ + ('etc/NetworkManager/system-connections/eth0.nmconnection', {})] + nics_info = [{'name': 'eth0'}] + + self.morphing_tools._write_dhcp_net_config(nics_info) + + mock_disable_predictable_nic_names.assert_called_once() + mock_nm_preserver.get_ethernet_keyfiles.assert_called_once_with() + mock_write_nmconnection_configs.assert_called_once_with(nics_info) + mock_write_nic_configs.assert_not_called() + + @mock.patch.object(suse.BaseSUSEMorphingTools, '_write_dhcp_net_config') + def test_set_net_config_dhcp(self, mock_write_dhcp_net_config): + nics_info = [{'name': 'eth0'}] + + self.morphing_tools.set_net_config(nics_info, dhcp=True) + + mock_write_dhcp_net_config.assert_called_once_with(nics_info) + + @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)