#
# 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()