From e6f950bd62779a261a682846f39baaf62b4aa756 Mon Sep 17 00:00:00 2001 From: Daniel Vincze Date: Mon, 11 Dec 2023 16:42:10 +0200 Subject: [PATCH] Refactor GRUB2 console settings setup for migrated Linux machines This patch refactors the setup of GRUB2 console settings into `BaseLinuxOSMorphingTools`, so more providers could use it directly. --- coriolis/osmorphing/base.py | 169 ++++++++++++++++++++++++++++++++- coriolis/osmorphing/centos.py | 2 + coriolis/osmorphing/debian.py | 3 + coriolis/osmorphing/openwrt.py | 3 + coriolis/osmorphing/redhat.py | 20 +++- coriolis/osmorphing/rocky.py | 2 + coriolis/osmorphing/suse.py | 22 ++++- 7 files changed, 218 insertions(+), 3 deletions(-) diff --git a/coriolis/osmorphing/base.py b/coriolis/osmorphing/base.py index 95c078126..fbf7445e3 100644 --- a/coriolis/osmorphing/base.py +++ b/coriolis/osmorphing/base.py @@ -14,7 +14,7 @@ from coriolis import exception from coriolis import utils - +GRUB2_SERIAL = "serial --word=8 --stop=1 --speed=%d --parity=%s --unit=0" LOG = logging.getLogger(__name__) @@ -244,6 +244,9 @@ def pre_packages_uninstall(self, package_names): def post_packages_uninstall(self, package_names): self._restore_resolv_conf() + def get_update_grub2_command(self): + raise NotImplementedError() + def _test_path(self, chroot_path): path = os.path.join(self._os_root_dir, chroot_path) return utils.test_ssh_path(self._ssh, path) @@ -401,3 +404,167 @@ def _configure_cloud_init_user_retention(self): raise exception.CoriolisException( "Failed to reconfigure cloud-init to retain user " "credentials. Error was: %s" % str(err)) from err + + def _test_path_chroot(self, path): + # This method uses _exec_cmd_chroot() instead of SFTP stat() + # because in some situations, the SSH user used may not have + # execute rights on one or more of the folders that lead up + # to the file we are testing. In such cases, you simply get + # a permission denied error. Using _exec_cmd_chroot(), + # ensures you always run as root. + if path.startswith('/') is False: + path = "/%s" % path + exists = self._exec_cmd_chroot( + '[ -f "%s" ] && echo 1 || echo 0' % path).decode().rstrip('\n') + return exists == "1" + + def _read_file_sudo(self, chroot_path): + if chroot_path.startswith("/") is False: + chroot_path = "/%s" % chroot_path + contents = self._exec_cmd_chroot( + 'cat %s' % chroot_path) + return contents + + def _read_grub_config(self, config): + if self._test_path_chroot(config) is False: + raise IOError("could not find %s" % config) + contents = self._read_file_sudo(config).decode() + ret = {} + for line in contents.split('\n'): + if line.startswith("#"): + continue + details = line.split("=", 1) + if len(details) != 2: + continue + ret[details[0]] = details[1].strip('"') + return ret + + def _get_grub_config_obj(self, grub_conf=None): + grub_conf = grub_conf or "/etc/default/grub" + if self._test_path_chroot(grub_conf) is False: + raise IOError("could not find %s" % grub_conf) + tmp_file = self._exec_cmd_chroot("mktemp").decode().rstrip('\n') + self._exec_cmd_chroot( + "/bin/cp -fp %s %s" % (grub_conf, tmp_file)) + config_file = self._read_grub_config(tmp_file) + config_obj = { + "source": grub_conf, + "location": tmp_file, + "contents": config_file, + } + return config_obj + + def _validate_grub_config_obj(self, config_obj): + if type(config_obj) is not dict: + raise ValueError("invalid configObj") + + missing = [] + + for key in ("location", "source", "contents"): + if not config_obj.get(key): + missing.append(key) + + if len(missing) > 0: + raise ValueError( + "Invalid configObj. Missing: %s" % ", ".join(missing)) + + def set_grub_value(self, option, value, config_obj, replace=True): + self._validate_grub_config_obj(config_obj) + + def append_to_cfg(opt, val): + cmd = "sed -ie '$a%(o)s=\"%(v)s\"' %(cfg)s" % { + "o": opt, + "v": val, + "cfg": config_obj["location"] + } + self._exec_cmd_chroot(cmd) + + def replace_in_cfg(opt, val): + cmd = "sed -i 's|^%(o)s=.*|%(o)s=\"%(v)s\"|g' %(cfg)s" % { + "o": opt, + "v": val, + "cfg": config_obj["location"] + } + self._exec_cmd_chroot(cmd) + + if config_obj["contents"].get(option, False): + if replace: + replace_in_cfg(option, value) + else: + append_to_cfg(option, value) + cfg = self._read_file_sudo(config_obj["location"]).decode() + LOG.warning("TEMP CONFIG IS: %r" % cfg) + + def _set_grub2_cmdline(self, config_obj, options, clobber=False): + kernel_cmd_def = config_obj["contents"].get( + "GRUB_CMDLINE_LINUX_DEFAULT") + kernel_cmd = config_obj["contents"].get( + "GRUB_CMDLINE_LINUX") + replace = kernel_cmd is not None + + if clobber: + opt = " ".join(options) + self.set_grub_value( + "GRUB_CMDLINE_LINUX", opt, config_obj, replace=replace) + return + kernel_cmd_def = kernel_cmd_def or "" + kernel_cmd = kernel_cmd or "" + to_add = [] + for option in options: + if option not in kernel_cmd_def and option not in kernel_cmd: + to_add.append(option) + if len(to_add): + kernel_cmd = "%s %s" % (kernel_cmd, " ".join(to_add)) + self.set_grub_value( + "GRUB_CMDLINE_LINUX", kernel_cmd, config_obj, replace=replace) + + def _execute_update_grub(self): + update_cmd = self.get_update_grub2_command() + self._exec_cmd_chroot(update_cmd) + + def _apply_grub2_config(self, config_obj, + execute_update_grub=True): + self._validate_grub_config_obj(config_obj) + self._exec_cmd_chroot( + "mv -f %s %s" % ( + config_obj["location"], config_obj["source"])) + if execute_update_grub: + self._execute_update_grub() + + def _set_grub2_console_settings(self, consoles=None, speed=None, + parity=None, grub_conf=None, + execute_update_grub=True): + # This method updates the GRUB2 config file and adds serial console + # support. + # + # param: consoles: list: Consoles you wish to enable on the migrated + # instance. By default, this method ensures: tty0 and ttyS0 + # param: speed: int: Baud rate for the serial console + # param: parity: string: Options are: no, odd, even + # param: grub_conf: string: Path to grub2 config + + valid_parity = ["no", "odd", "even"] + if parity and parity not in valid_parity: + raise ValueError( + "Valid values for parity are: %s" % ", ".join(valid_parity)) + + speed = speed or 115200 + parity = parity or "no" + consoles = consoles or ["tty0", "ttyS0"] + + if type(consoles) is not list: + raise ValueError("invalid consoles option") + + serial_cmd = GRUB2_SERIAL % (int(speed), parity) + + config_obj = self._get_grub_config_obj(grub_conf) + self.set_grub_value("GRUB_SERIAL_COMMAND", serial_cmd, config_obj) + + options = [] + for console in consoles: + c = "console=%s" % console + options.append(c) + + self._set_grub2_cmdline(config_obj, options) + self._apply_grub2_config( + config_obj, execute_update_grub) diff --git a/coriolis/osmorphing/centos.py b/coriolis/osmorphing/centos.py index c6a7726ee..bd4d8ee46 100644 --- a/coriolis/osmorphing/centos.py +++ b/coriolis/osmorphing/centos.py @@ -12,6 +12,8 @@ class BaseCentOSMorphingTools(redhat.BaseRedHatMorphingTools): + UEFI_GRUB_LOCATION = "/boot/efi/EFI/centos" + @classmethod def check_os_supported(cls, detected_os_info): supported_oses = [ diff --git a/coriolis/osmorphing/debian.py b/coriolis/osmorphing/debian.py index 7daa7f206..3b4d10a4a 100644 --- a/coriolis/osmorphing/debian.py +++ b/coriolis/osmorphing/debian.py @@ -58,6 +58,9 @@ def disable_predictable_nic_names(self): self._write_file_sudo("etc/default/grub", cfg.dump()) self._exec_cmd_chroot("/usr/sbin/update-grub") + def get_update_grub2_command(self): + return "update-grub" + def _compose_interfaces_config(self, nics_info): fp = StringIO() fp.write(LO_NIC_TPL) diff --git a/coriolis/osmorphing/openwrt.py b/coriolis/osmorphing/openwrt.py index 59d8e0394..63acbbf08 100644 --- a/coriolis/osmorphing/openwrt.py +++ b/coriolis/osmorphing/openwrt.py @@ -34,3 +34,6 @@ def post_packages_uninstall(self, package_names): def set_net_config(self, nics_info, dhcp): pass + + def get_update_grub2_command(self): + raise NotImplementedError() diff --git a/coriolis/osmorphing/redhat.py b/coriolis/osmorphing/redhat.py index 269628475..ea2e7f4fc 100644 --- a/coriolis/osmorphing/redhat.py +++ b/coriolis/osmorphing/redhat.py @@ -13,7 +13,6 @@ from coriolis.osmorphing.osdetect import redhat as redhat_detect from coriolis import utils - RED_HAT_DISTRO_IDENTIFIER = redhat_detect.RED_HAT_DISTRO_IDENTIFIER LOG = logging.getLogger(__name__) @@ -42,6 +41,8 @@ class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools): _NETWORK_SCRIPTS_PATH = "etc/sysconfig/network-scripts" + BIOS_GRUB_LOCATION = "/boot/grub2" + UEFI_GRUB_LOCATION = "/boot/efi/EFI/redhat" @classmethod def check_os_supported(cls, detected_os_info): @@ -63,6 +64,23 @@ def disable_predictable_nic_names(self): cmd = 'grubby --update-kernel=ALL --args="%s"' self._exec_cmd_chroot(cmd % "net.ifnames=0 biosdevname=0") + def get_update_grub2_command(self): + location = self._get_grub2_cfg_location() + return "grub2-mkconfig -o %s" % location + + def _get_grub2_cfg_location(self): + self._exec_cmd_chroot("mount /boot || true") + self._exec_cmd_chroot("mount /boot/efi || true") + uefi_cfg = os.path.join(self.UEFI_GRUB_LOCATION, "grub.cfg") + bios_cfg = os.path.join(self.BIOS_GRUB_LOCATION, "grub.cfg") + if self._test_path_chroot(uefi_cfg): + return uefi_cfg + if self._test_path_chroot(bios_cfg): + return bios_cfg + raise Exception( + "could not determine grub location." + " boot partition not mounted?") + def _get_net_ifaces_info(self, ifcfgs_ethernet, mac_addresses): net_ifaces_info = [] diff --git a/coriolis/osmorphing/rocky.py b/coriolis/osmorphing/rocky.py index 997cb4d3e..00a6384c3 100644 --- a/coriolis/osmorphing/rocky.py +++ b/coriolis/osmorphing/rocky.py @@ -10,6 +10,8 @@ class BaseRockyLinuxMorphingTools(centos.BaseCentOSMorphingTools): + UEFI_GRUB_LOCATION = "/boot/efi/EFI/rocky" + @classmethod def check_os_supported(cls, detected_os_info): if detected_os_info['distribution_name'] != ( diff --git a/coriolis/osmorphing/suse.py b/coriolis/osmorphing/suse.py index 6009fec0c..5ef6bda04 100644 --- a/coriolis/osmorphing/suse.py +++ b/coriolis/osmorphing/suse.py @@ -2,6 +2,7 @@ # All Rights Reserved. import copy +import os import re import uuid @@ -12,7 +13,6 @@ from coriolis.osmorphing.osdetect import suse as suse_detect from coriolis import utils - LOG = logging.getLogger(__name__) DETECTED_SUSE_RELEASE_FIELD_NAME = suse_detect.DETECTED_SUSE_RELEASE_FIELD_NAME @@ -26,6 +26,9 @@ class BaseSUSEMorphingTools(base.BaseLinuxOSMorphingTools): + BIOS_GRUB_LOCATION = "/boot/grub2" + UEFI_GRUB_LOCATION = "/boot/efi/EFI/suse" + @classmethod def get_required_detected_os_info_fields(cls): common_fields = super( @@ -62,6 +65,23 @@ def set_net_config(self, nics_info, dhcp): # TODO(alexpilotti): add networking support pass + def get_update_grub2_command(self): + location = self._get_grub2_cfg_location() + return "grub2-mkconfig -o %s" % location + + def _get_grub2_cfg_location(self): + self._exec_cmd_chroot("mount /boot || true") + self._exec_cmd_chroot("mount /boot/efi || true") + uefi_cfg = os.path.join(self.UEFI_GRUB_LOCATION, "grub.cfg") + bios_cfg = os.path.join(self.BIOS_GRUB_LOCATION, "grub.cfg") + if self._test_path_chroot(uefi_cfg): + return uefi_cfg + if self._test_path_chroot(bios_cfg): + return bios_cfg + raise Exception( + "could not determine grub location." + " boot partition not mounted?") + def _run_dracut(self): self._exec_cmd_chroot("dracut --regenerate-all -f")