diff --git a/coriolis/osmorphing/base.py b/coriolis/osmorphing/base.py index 7e7c08cd..1422debd 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: @@ -467,6 +500,12 @@ 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 _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 869a2d21..71071929 100644 --- a/coriolis/osmorphing/redhat.py +++ b/coriolis/osmorphing/redhat.py @@ -23,40 +23,6 @@ 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" @@ -141,12 +107,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: @@ -175,7 +135,7 @@ def _write_nic_configs(self, nics_info): cfg_path = "%s/ifcfg-%s" % (self._NETWORK_SCRIPTS_PATH, dev_name) self._write_file_sudo( cfg_path, - IFCFG_TEMPLATE % { + base.IFCFG_TEMPLATE % { "device_name": dev_name, "nm_controlled": self._get_ifcfg_nm_controlled(), }) @@ -197,7 +157,7 @@ def _write_nmconnection_configs(self, nics_info, nmconnection_files): self._NM_CONNECTIONS_PATH, dev_name) self._write_file_sudo( cfg_path, - NMCONNECTION_TEMPLATE % { + base.NMCONNECTION_TEMPLATE % { "device_name": dev_name, "connection_uuid": str(uuid.uuid4()), }) diff --git a/coriolis/osmorphing/suse.py b/coriolis/osmorphing/suse.py index 7aaf755d..1d95dc05 100644 --- a/coriolis/osmorphing/suse.py +++ b/coriolis/osmorphing/suse.py @@ -61,12 +61,102 @@ 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_ifcfg_nm_controlled(self): + if self._version_supported_util(self._version, minimum=15): + return "yes" + return "no" + + def _backup_nmconnection_files(self, backup_file_suffix=".bak"): + """Back up all existing nmconnection profiles.""" + if not self._test_path(self._NM_CONNECTIONS_PATH): + return + for cfg_path in self._get_nmconnection_files( + self._NM_CONNECTIONS_PATH): + 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_ifcfg_configs(self, device_names, backup_file_suffix=".bak"): + """Back up ifcfg profiles for the given device names.""" + for dev_name in device_names: + cfg_path = "%s/ifcfg-%s" % (self._NETWORK_SCRIPTS_PATH, dev_name) + if self._test_path(cfg_path): + 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): + 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, + base.IFCFG_TEMPLATE % { + "device_name": dev_name, + "nm_controlled": self._get_ifcfg_nm_controlled(), + }) + + def _write_nmconnection_configs(self, nics_info, nmconnection_files=None): + self._backup_nmconnection_files() + device_names = ["eth%d" % idx for idx, _ in enumerate(nics_info)] + self._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, + base.NMCONNECTION_TEMPLATE % { + "device_name": dev_name, + "connection_uuid": str(uuid.uuid4()), + }) + self._exec_cmd_chroot("chmod 600 /%s" % cfg_path) 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_redhat.py b/coriolis/tests/osmorphing/test_redhat.py index 8d9d054a..241493bf 100644 --- a/coriolis/tests/osmorphing/test_redhat.py +++ b/coriolis/tests/osmorphing/test_redhat.py @@ -162,14 +162,14 @@ def test_write_nic_configs( 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", }, @@ -189,7 +189,7 @@ def test_write_nic_configs_rhel8( mock_backup_all_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", }, @@ -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( @@ -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( diff --git a/coriolis/tests/osmorphing/test_suse.py b/coriolis/tests/osmorphing/test_suse.py index b2372b16..d91f9684 100644 --- a/coriolis/tests/osmorphing/test_suse.py +++ b/coriolis/tests/osmorphing/test_suse.py @@ -434,3 +434,268 @@ 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, '_exec_cmd_chroot') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_get_nmconnection_files') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__backup_nmconnection_files( + self, mock_test_path, mock_get_nmconnection_files, + mock_exec_cmd_chroot): + mock_test_path.return_value = True + # All nmconnection profiles must be backed up, not only ethernet ones. + mock_get_nmconnection_files.return_value = [ + 'etc/NetworkManager/system-connections/eth0.nmconnection', + 'etc/NetworkManager/system-connections/wifi.nmconnection'] + + with self.assertLogs('coriolis.osmorphing.suse', level=logging.DEBUG): + self.morphing_tools._backup_nmconnection_files() + + mock_get_nmconnection_files.assert_called_once_with( + "etc/NetworkManager/system-connections") + 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/wifi.nmconnection" ' + '"etc/NetworkManager/system-connections/' + 'wifi.nmconnection.bak"'), + ]) + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object( + base.BaseLinuxOSMorphingTools, '_get_nmconnection_files') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__backup_nmconnection_files_no_path( + self, mock_test_path, mock_get_nmconnection_files, + mock_exec_cmd_chroot): + mock_test_path.return_value = False + + self.morphing_tools._backup_nmconnection_files() + + mock_get_nmconnection_files.assert_not_called() + mock_exec_cmd_chroot.assert_not_called() + + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot') + @mock.patch.object(base.BaseLinuxOSMorphingTools, '_test_path') + def test__backup_ifcfg_configs( + self, mock_test_path, mock_exec_cmd_chroot): + # Only the targeted devices that actually exist are backed up. + mock_test_path.side_effect = [True, False] + + with self.assertLogs('coriolis.osmorphing.suse', level=logging.DEBUG): + self.morphing_tools._backup_ifcfg_configs(['eth0', 'eth1']) + + mock_test_path.assert_has_calls([ + mock.call("etc/sysconfig/network-scripts/ifcfg-eth0"), + mock.call("etc/sysconfig/network-scripts/ifcfg-eth1"), + ]) + mock_exec_cmd_chroot.assert_called_once_with( + 'mv "etc/sysconfig/network-scripts/ifcfg-eth0" ' + '"etc/sysconfig/network-scripts/ifcfg-eth0.bak"') + + 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", + 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, '_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", + base.IFCFG_TEMPLATE % { + "device_name": "eth0", + "nm_controlled": "yes", + }, + ) + + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_backup_ifcfg_configs') + @mock.patch.object( + suse.BaseSUSEMorphingTools, '_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_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() + mock_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, '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)