# # rescue.py - anaconda rescue mode setup # # Copyright (C) 2015 Red Hat, Inc. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # 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 . # from pyanaconda.core import util from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.constants import ANACONDA_CLEANUP, THREAD_STORAGE, QUIT_MESSAGE from pyanaconda.modules.common.constants.objects import DEVICE_TREE from pyanaconda.modules.common.constants.services import STORAGE from pyanaconda.modules.common.errors.storage import MountFilesystemError from pyanaconda.modules.common.structures.storage import OSData, DeviceFormatData from pyanaconda.modules.common.task import sync_run_task from pyanaconda.core.threads import thread_manager from pyanaconda.flags import flags from pyanaconda.core.i18n import _, N_ from pyanaconda.kickstart import runPostScripts from pyanaconda.ui.tui import tui_quit_callback from pyanaconda.ui.tui.spokes import NormalTUISpoke from pykickstart.constants import KS_REBOOT, KS_SHUTDOWN from simpleline import App from simpleline.render.adv_widgets import YesNoDialog, PasswordDialog from simpleline.render.containers import ListColumnContainer from simpleline.render.prompt import Prompt from simpleline.render.screen import InputState from simpleline.render.screen_handler import ScreenHandler from simpleline.render.widgets import TextWidget, CheckboxWidget import os import shutil import time from enum import Enum from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) __all__ = ["RescueModeSpoke", "RootSelectionSpoke", "RescueStatusAndShellSpoke"] def makeFStab(instPath=""): """Make the fs tab.""" if os.access("/proc/mounts", os.R_OK): f = open("/proc/mounts", "r") buf = f.read() f.close() else: buf = "" try: f = open(instPath + "/etc/fstab", "a") if buf: f.write(buf) f.close() except OSError as e: log.info("failed to write /etc/fstab: %s", e) def makeResolvConf(instPath): """Make the resolv.conf file in the chroot.""" if conf.target.is_image: return if not os.access("/etc/resolv.conf", os.R_OK): return if os.access("%s/etc/resolv.conf" %(instPath,), os.R_OK): f = open("%s/etc/resolv.conf" %(instPath,), "r") buf = f.read() f.close() else: buf = "" # already have a nameserver line, don't worry about it if buf.find("nameserver") != -1: return f = open("/etc/resolv.conf", "r") buf = f.read() f.close() # no nameserver, we can't do much about it if buf.find("nameserver") == -1: return shutil.copyfile("%s/etc/resolv.conf" %(instPath,), "%s/etc/resolv.conf.bak" %(instPath,)) f = open("%s/etc/resolv.conf" %(instPath,), "w+") f.write(buf) f.close() def create_etc_symlinks(): """I don't know why I live. Maybe I should be killed.""" for f in ["services", "protocols", "group", "man.config", "nsswitch.conf", "selinux", "mke2fs.conf"]: try: os.symlink('/mnt/runtime/etc/' + f, '/etc/' + f) except OSError: log.debug("Failed to create symlink for /mnt/runtime/etc/%s", f) class RescueModeStatus(Enum): """Status of rescue mode environment.""" NOT_SET = "not set" MOUNTED = "mounted" MOUNT_FAILED = "mount failed" ROOT_NOT_FOUND = "root not found" class Rescue(object): """Rescue mode module. Provides interface to: - find and unlock encrypted devices - find existing systems - mount selected existing system and run kickstart scripts - run interactive shell - finish the environment (reboot) Dependencies: - storage module - storage initialization thread Initialization: rescue_data - data of rescue command seen in kickstart reboot - flag for rebooting after finishing bool scritps - kickstart scripts to be run after mounting root """ def __init__(self, rescue_data=None, reboot=False, scripts=None, rescue_nomount=True): self._storage_proxy = STORAGE.get_proxy() self._device_tree_proxy = STORAGE.get_proxy(DEVICE_TREE) self._scripts = scripts self.reboot = reboot self.automated = False self.mount = False self.ro = False self.autorelabel = False self.status = RescueModeStatus.NOT_SET self.error = None if rescue_data: self.automated = True self.mount = not (rescue_data.nomount or rescue_nomount) self.ro = rescue_data.romount def initialize(self): thread_manager.wait(THREAD_STORAGE) create_etc_symlinks() def find_roots(self): """List of found roots.""" task_path = self._device_tree_proxy.FindExistingSystemsWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) # Collect existing systems. roots = OSData.from_structure_list( self._device_tree_proxy.GetExistingSystems() ) # Ignore systems without a root device. roots = [r for r in roots if r.get_root_device()] if not roots: self.status = RescueModeStatus.ROOT_NOT_FOUND log.debug("These systems were found: %s", str(roots)) return roots # TODO separate running post scripts? def mount_root(self, root): """Mounts selected root and runs scripts.""" # mount root fs try: task_path = self._device_tree_proxy.MountExistingSystemWithTask( root.get_root_device(), self.ro ) task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) log.info("System has been mounted under: %s", conf.target.system_root) except MountFilesystemError as e: log.error("Mounting system under %s failed: %s", conf.target.system_root, e) self.status = RescueModeStatus.MOUNT_FAILED self.error = e return False # turn on selinux also if conf.security.selinux: # we have to catch the possible exception, because we # support read-only mounting try: fd = open("%s/.autorelabel" % conf.target.system_root, "w+") fd.close() self.autorelabel = True except OSError as e: log.warning("Error turning on selinux: %s", e) # set a libpath to use mounted fs libdirs = os.environ.get("LD_LIBRARY_PATH", "").split(":") mounted = ["/mnt/sysimage%s" % ldir for ldir in libdirs] util.setenv("LD_LIBRARY_PATH", ":".join(libdirs + mounted)) # do we have bash? try: if os.access("/usr/bin/bash", os.R_OK): os.symlink("/usr/bin/bash", "/bin/bash") except OSError as e: log.error("Error symlinking bash: %s", e) # make resolv.conf in chroot if not self.ro: try: makeResolvConf(conf.target.system_root) except OSError as e: log.error("Error making resolv.conf: %s", e) # create /etc/fstab in ramdisk so it's easier to work with RO mounted fs makeFStab() # run %post if we've mounted everything if not self.ro and self._scripts: runPostScripts(self._scripts) self.status = RescueModeStatus.MOUNTED return True def get_locked_device_names(self): """Get a list of names of locked LUKS devices. All LUKS devices are considered locked. """ device_names = [] for device_name in self._device_tree_proxy.GetDevices(): format_data = DeviceFormatData.from_structure( self._device_tree_proxy.GetFormatData(device_name) ) if not format_data.type == "luks": continue device_names.append(device_name) return device_names def unlock_device(self, device_name, passphrase): """Unlocks LUKS device.""" return self._device_tree_proxy.UnlockDevice(device_name, passphrase) def run_shell(self): """Launch a shell.""" if os.path.exists("/bin/bash"): util.execConsole() else: # TODO: FIXME -> move to UI (check via module api?) print(_("Unable to find /bin/bash to execute! Not starting shell.")) time.sleep(5) def finish(self, delay=0): """Finish rescue mode with optional delay.""" time.sleep(delay) if self.reboot: util.execWithRedirect("systemctl", ["--no-wall", "reboot"]) class RescueModeSpoke(NormalTUISpoke): """UI offering mounting existing installation roots in rescue mode.""" # If it acts like a spoke and looks like a spoke, is it a spoke? Not # always. This is independent of any hub(s), so pass in some fake data def __init__(self, rescue): super().__init__(data=None, storage=None, payload=None) self.title = N_("Rescue") self._container = None self._rescue = rescue def refresh(self, args=None): super().refresh(args) msg = _("The rescue environment will now attempt " "to find your Linux installation and mount it under " "the directory : %s. You can then make any changes " "required to your system. Choose '1' to proceed with " "this step.\nYou can choose to mount your file " "systems read-only instead of read-write by choosing " "'2'.\nIf for some reason this process does not work " "choose '3' to skip directly to a shell.\n\n") % (conf.target.system_root) self.window.add_with_separator(TextWidget(msg)) self._container = ListColumnContainer(1) self._container.add(TextWidget(_("Continue")), self._read_write_mount_callback) self._container.add(TextWidget(_("Read-only mount")), self._read_only_mount_callback) self._container.add(TextWidget(_("Skip to shell")), self._skip_to_shell_callback) self._container.add(TextWidget(_("Quit (Reboot)")), self._quit_callback) self.window.add_with_separator(self._container) def _read_write_mount_callback(self, data): self._mount_and_prompt_for_shell() def _read_only_mount_callback(self, data): self._rescue.ro = True self._mount_and_prompt_for_shell() def _skip_to_shell_callback(self, data): self._show_result_and_prompt_for_shell() def _quit_callback(self, data): d = YesNoDialog(_(QUIT_MESSAGE)) ScreenHandler.push_screen_modal(d) self.redraw() if d.answer: self._rescue.reboot = True self._rescue.finish() def _mount_and_prompt_for_shell(self): self._rescue.mount = True self._mount_root() self._show_result_and_prompt_for_shell() def prompt(self, args=None): """ Override the default TUI prompt.""" if self._rescue.automated: if self._rescue.mount: self._mount_root() self._show_result_and_prompt_for_shell() return None return Prompt() def input(self, args, key): """Override any input so we can launch rescue mode.""" if self._container.process_user_input(key): return InputState.PROCESSED else: return InputState.DISCARDED def _mount_root(self): # decrypt all luks devices self._unlock_devices() roots = self._rescue.find_roots() if not roots: return if len(roots) == 1: root = roots[0] else: # have to prompt user for which root to mount root_spoke = RootSelectionSpoke(roots) ScreenHandler.push_screen_modal(root_spoke) self.redraw() root = root_spoke.selection self._rescue.mount_root(root) def _show_result_and_prompt_for_shell(self): new_spoke = RescueStatusAndShellSpoke(self._rescue) ScreenHandler.push_screen_modal(new_spoke) self.close() def _unlock_devices(self): """Attempt to unlock all locked LUKS devices.""" passphrase = None for device_name in self._rescue.get_locked_device_names(): while True: if passphrase is None: dialog = PasswordDialog(device_name) ScreenHandler.push_screen_modal(dialog) if not dialog.answer: break passphrase = dialog.answer.strip() if self._rescue.unlock_device(device_name, passphrase): break passphrase = None def apply(self): """Move along home.""" pass class RescueStatusAndShellSpoke(NormalTUISpoke): """UI displaying status of rescue mode mount and prompt for shell.""" def __init__(self, rescue): super().__init__(data=None, storage=None, payload=None) self.title = N_("Rescue Shell") self._rescue = rescue @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) umount_msg = _("Run %s to unmount the system when you are finished.") % ANACONDA_CLEANUP exit_reboot_msg = _("When finished, please exit from the shell and your " "system will reboot.\n") text = None if self._rescue.mount: status = self._rescue.status if status == RescueModeStatus.MOUNTED: if self._rescue.reboot: finish_msg = exit_reboot_msg else: finish_msg = umount_msg autorelabel_msg = (_("Warning: The rescue shell will trigger SELinux autorelabel " "on the subsequent boot. Add \"enforcing=0\" on the kernel " "command line for autorelabel to work properly.\n") if self._rescue.autorelabel else "") text = TextWidget(_("Your system has been mounted under %(mountpoint)s.\n\n" "If you would like to make the root of your system the " "root of the active system, run the command:\n\n" "\tchroot %(mountpoint)s\n\n") % {"mountpoint": conf.target.system_root} + autorelabel_msg + finish_msg) elif status == RescueModeStatus.MOUNT_FAILED: if self._rescue.reboot: finish_msg = exit_reboot_msg else: finish_msg = umount_msg msg = _( "An error occurred trying to mount some or all of your system: " "{message}\n\nSome of it may be mounted under {path}.").format( message=str(self._rescue.error), path=conf.target.system_root ) text = TextWidget(msg + " " + finish_msg) elif status == RescueModeStatus.ROOT_NOT_FOUND: if self._rescue.reboot: finish_msg = exit_reboot_msg else: finish_msg = "" text = TextWidget(_("No Linux systems found.\n") + finish_msg) else: if self._rescue.reboot: finish_msg = exit_reboot_msg else: finish_msg = "" text = TextWidget(_("Not mounting the system.\n") + finish_msg) self.window.add(text) def prompt(self, args=None): """ Override the default TUI prompt.""" if self._rescue.automated: if self._rescue.reboot and self._rescue.status == RescueModeStatus.ROOT_NOT_FOUND: delay = 5 else: delay = 0 self._rescue.run_shell() self._rescue.finish(delay=delay) return None return Prompt(_("Please press %s to get a shell") % Prompt.ENTER) def input(self, args, key): """Move along home.""" self._rescue.run_shell() self._rescue.finish() return InputState.PROCESSED def apply(self): pass class RootSelectionSpoke(NormalTUISpoke): """UI for selection of installed system root to be mounted.""" def __init__(self, roots): super().__init__(data=None, storage=None, payload=None) self.title = N_("Root Selection") self._roots = roots self._selection = roots[0] self._container = None @property def selection(self): """The selected root fs to mount.""" return self._selection @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for root in self._roots: box = CheckboxWidget( title="{} on {}".format(root.os_name, root.get_root_device()), completed=(self._selection == root) ) self._container.add(box, self._select_root, root) message = _("The following installations were discovered on your system.") self.window.add_with_separator(TextWidget(message)) self.window.add_with_separator(self._container) def _select_root(self, root): self._selection = root def prompt(self, args=None): """ Override the default TUI prompt.""" prompt = Prompt() prompt.add_continue_option() return prompt def input(self, args, key): """Override any input so we can launch rescue mode.""" if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW elif key == Prompt.CONTINUE: return InputState.PROCESSED_AND_CLOSE else: return key def apply(self): """Define the abstract method.""" pass def start_rescue_mode_ui(anaconda): """Start the rescue mode UI.""" ksdata_rescue = None if anaconda.ksdata.rescue.seen: ksdata_rescue = anaconda.ksdata.rescue scripts = anaconda.ksdata.scripts rescue_nomount = anaconda.opts.rescue_nomount reboot = True if conf.target.is_image: reboot = False if flags.automatedInstall and anaconda.ksdata.reboot.action not in [KS_REBOOT, KS_SHUTDOWN]: reboot = False rescue = Rescue(ksdata_rescue, reboot, scripts, rescue_nomount) rescue.initialize() # We still want to choose from multiple roots, or unlock encrypted devices # if needed, so we run UI even for kickstarts (automated install). App.initialize() loop = App.get_event_loop() loop.set_quit_callback(tui_quit_callback) spoke = RescueModeSpoke(rescue) ScreenHandler.schedule_screen(spoke) App.run()