anaconda/anaconda-40.22.3.13/pyanaconda/kickstart.py

443 lines
15 KiB
Python
Raw Normal View History

2024-11-14 21:39:56 -08:00
#
# kickstart.py: kickstart install support
#
# Copyright (C) 1999-2016
# 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 <http://www.gnu.org/licenses/>.
#
import glob
import os
import os.path
import sys
import tempfile
import time
import warnings
from contextlib import contextmanager
from pyanaconda.anaconda_loggers import get_module_logger, get_stdout_logger
from pyanaconda.core import util
from pyanaconda.core.path import open_with_perm
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.kickstart import VERSION, commands as COMMANDS
from pyanaconda.core.kickstart.specification import KickstartSpecification
from pyanaconda.core.constants import IPMI_ABORTED
from pyanaconda.errors import ScriptError, errorHandler
from pyanaconda.flags import flags
from pyanaconda.core.i18n import _
from pyanaconda.modules.common.constants.services import BOSS
from pyanaconda.modules.common.structures.kickstart import KickstartReport
from pykickstart.base import KickstartCommand, RemovedCommand
from pykickstart.constants import KS_SCRIPT_POST, KS_SCRIPT_PRE, KS_SCRIPT_TRACEBACK, KS_SCRIPT_PREINSTALL
from pykickstart.errors import KickstartError, KickstartParseWarning
from pykickstart.ko import KickstartObject
from pykickstart.parser import KickstartParser
from pykickstart.parser import Script as KSScript
from pykickstart.sections import NullSection, PostScriptSection, PreScriptSection, \
PreInstallScriptSection, OnErrorScriptSection, TracebackScriptSection, Section
from pykickstart.version import returnClassForVersion
log = get_module_logger(__name__)
stdoutLog = get_stdout_logger()
# kickstart parsing and kickstart script
script_log = log.getChild("script")
parsing_log = log.getChild("parsing")
@contextmanager
def check_kickstart_error():
try:
yield
except KickstartError as e:
# We do not have an interface here yet, so we cannot use our error
# handling callback.
print(e)
util.ipmi_report(IPMI_ABORTED)
sys.exit(1)
class AnacondaKSScript(KSScript):
""" Execute a kickstart script
This will write the script to a file named /tmp/ks-script- before
execution.
Output is logged by the program logger, the path specified by --log
or to /tmp/ks-script-\\*.log
"""
def run(self, chroot):
""" Run the kickstart script
@param chroot directory path to chroot into before execution
"""
if self.inChroot:
scriptRoot = chroot
else:
scriptRoot = "/"
(fd, path) = tempfile.mkstemp("", "ks-script-", scriptRoot + "/tmp")
os.write(fd, self.script.encode("utf-8"))
os.close(fd)
os.chmod(path, 0o700)
# Always log stdout/stderr from scripts. Using --log just lets you
# pick where it goes. The script will also be logged to program.log
# because of execWithRedirect.
if self.logfile:
if self.inChroot:
messages = "%s/%s" % (scriptRoot, self.logfile)
else:
messages = self.logfile
d = os.path.dirname(messages)
if not os.path.exists(d):
os.makedirs(d)
else:
# Always log outside the chroot, we copy those logs into the
# chroot later.
messages = "/tmp/%s.log" % os.path.basename(path)
with open_with_perm(messages, "w", 0o600) as fp:
rc = util.execWithRedirect(self.interp, ["/tmp/%s" % os.path.basename(path)],
stdout=fp,
root=scriptRoot)
if rc != 0:
script_log.error("Error code %s running the kickstart script at line %s", rc, self.lineno)
if self.errorOnFail:
err = ""
with open(messages, "r") as fp:
err = "".join(fp.readlines())
# Show error dialog even for non-interactive
flags.ksprompt = True
errorHandler.cb(ScriptError(self.lineno, err))
util.ipmi_report(IPMI_ABORTED)
sys.exit(0)
class AnacondaInternalScript(AnacondaKSScript):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._hidden = True
def __str__(self):
# Scripts that implement portions of anaconda (copying screenshots and
# log files, setfilecons, etc.) should not be written to the output
# kickstart file.
return ""
###
### SUBCLASSES OF PYKICKSTART COMMAND HANDLERS
###
class UselessSection(Section):
"""Kickstart section that was moved on DBus and doesn't do anything."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sectionOpen = kwargs.get("sectionOpen")
class UselessCommand(KickstartCommand):
"""Kickstart command that was moved on DBus and doesn't do anything.
Use this class to override the pykickstart command in our command map,
when we don't want the command to do anything.
"""
def __str__(self):
"""Generate this part of a kickstart file from the DBus module."""
return ""
def parse(self, args):
"""Do not parse anything.
We can keep this method for the checks if it is possible, but
it shouldn't parse anything.
"""
log.warning("Command %s will be parsed in DBus module.", self.currentCmd)
class UselessObject(KickstartObject):
"""Kickstart object that was moved on DBus and doesn't do anything."""
def __str__(self):
"""Generate this part of a kickstart file from the DBus module."""
return ""
###
### HANDLERS
###
# This is just the latest entry from pykickstart.handlers.control with all the
# classes we're overriding in place of the defaults.
class AnacondaKickstartSpecification(KickstartSpecification):
"""The kickstart specification of the main process."""
commands = {
"cmdline": COMMANDS.DisplayMode,
"eula": COMMANDS.Eula,
"graphical": COMMANDS.DisplayMode,
"halt": COMMANDS.Reboot,
"logging": COMMANDS.Logging,
"poweroff": COMMANDS.Reboot,
"reboot": COMMANDS.Reboot,
"rescue": COMMANDS.Rescue,
"shutdown": COMMANDS.Reboot,
"text": COMMANDS.DisplayMode,
"vnc": COMMANDS.Vnc,
}
@classmethod
def generate_command_map(cls, handler):
"""Generate a command map.
:param handler: a kickstart handler
:return: a map of command overrides
"""
command_map = dict(cls.commands)
for name, command in handler.commandMap.items():
# Ignore removed commands.
if issubclass(command, RemovedCommand):
continue
# Mark unspecified commands as useless.
if name not in command_map:
command_map[name] = UselessCommand
return command_map
@classmethod
def generate_data_map(cls, handler):
"""Generate a data map.
:param handler: a kickstart handler
:return: a map of data overrides
"""
return dict(cls.commands_data)
# Get the kickstart handler for the specified version.
superclass = returnClassForVersion(VERSION)
# Generate the command and data overrides.
specification = AnacondaKickstartSpecification
commandMap = specification.generate_command_map(superclass)
dataMap = specification.generate_data_map(superclass)
class AnacondaKSHandler(superclass):
def __init__(self, commandUpdates=None, dataUpdates=None):
if commandUpdates is None:
commandUpdates = commandMap
if dataUpdates is None:
dataUpdates = dataMap
super().__init__(commandUpdates=commandUpdates, dataUpdates=dataUpdates)
self.onPart = {}
# The %packages section is handled by the DBus module.
self.packages = UselessObject()
def __str__(self):
proxy = BOSS.get_proxy()
modules = proxy.GenerateKickstart().strip()
return super().__str__() + "\n" + modules
class AnacondaPreParser(KickstartParser):
# A subclass of KickstartParser that only looks for %pre scripts and
# sets them up to be run. All other scripts and commands are ignored.
def __init__(self, handler):
super().__init__(handler, missingIncludeIsFatal=False)
def handleCommand(self, lineno, args):
pass
def setupSections(self):
self.registerSection(PreScriptSection(self.handler, dataObj=AnacondaKSScript))
self.registerSection(NullSection(self.handler, sectionOpen="%pre-install"))
self.registerSection(NullSection(self.handler, sectionOpen="%post"))
self.registerSection(NullSection(self.handler, sectionOpen="%onerror"))
self.registerSection(NullSection(self.handler, sectionOpen="%traceback"))
self.registerSection(NullSection(self.handler, sectionOpen="%packages"))
self.registerSection(NullSection(self.handler, sectionOpen="%addon"))
class AnacondaKSParser(KickstartParser):
def __init__(self, handler, scriptClass=AnacondaKSScript):
self.scriptClass = scriptClass
super().__init__(handler)
def handleCommand(self, lineno, args):
if not self.handler:
return
return KickstartParser.handleCommand(self, lineno, args)
def setupSections(self):
self.registerSection(PreScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(PreInstallScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(PostScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(OnErrorScriptSection(self.handler, dataObj=self.scriptClass))
self.registerSection(UselessSection(self.handler, sectionOpen="%packages"))
self.registerSection(UselessSection(self.handler, sectionOpen="%addon"))
def preScriptPass(f):
# The first pass through kickstart file processing - look for %pre scripts
# and run them. This must come in a separate pass in case a script
# generates an included file that has commands for later.
ksparser = AnacondaPreParser(AnacondaKSHandler())
with check_kickstart_error():
ksparser.readKickstart(f)
# run %pre scripts
runPreScripts(ksparser.handler.scripts)
def parseKickstart(handler, f, strict_mode=False):
# preprocessing the kickstart file has already been handled in initramfs.
ksparser = AnacondaKSParser(handler)
kswarnings = []
showwarning = warnings.showwarning
def ksshowwarning(message, category, filename, lineno, file=None, line=None):
# Print the warning with default function.
showwarning(message, category, filename, lineno, file, line)
# Collect pykickstart warnings.
if issubclass(category, KickstartParseWarning):
kswarnings.append(message)
try:
# Process warnings differently in this part.
with warnings.catch_warnings():
# Set up the warnings module.
warnings.showwarning = ksshowwarning
warnings.simplefilter("always", category=KickstartParseWarning)
# Parse the kickstart file in DBus modules.
boss = BOSS.get_proxy()
report = KickstartReport.from_structure(
boss.ReadKickstartFile(f)
)
for warn in report.warning_messages:
warnings.warn(warn.message, KickstartParseWarning)
if not report.is_valid():
message = "\n\n".join(map(str, report.error_messages))
raise KickstartError(message)
# Parse the kickstart file in anaconda.
ksparser.readKickstart(f)
# Print kickstart warnings and error out if in strict mode
if kswarnings:
print(_("\nSome warnings occurred during reading the kickstart file:"))
for w in kswarnings:
print(str(w).strip())
if strict_mode:
raise KickstartError("Please modify your kickstart file to fix the warnings "
"or remove the `ksstrict` option.")
except KickstartError as e:
# We do not have an interface here yet, so we cannot use our error
# handling callback.
parsing_log.error(e)
# Print an error and terminate.
print(_("\nAn error occurred during reading the kickstart file:"
"\n%s\n\nThe installer will now terminate.") % str(e).strip())
util.ipmi_report(IPMI_ABORTED)
time.sleep(10)
sys.exit(1)
def appendPostScripts(ksdata):
scripts = ""
# Read in all the post script snippets to a single big string.
for fn in sorted(glob.glob("/usr/share/anaconda/post-scripts/*ks")):
f = open(fn, "r")
scripts += f.read()
f.close()
# Then parse the snippets against the existing ksdata. We can do this
# because pykickstart allows multiple parses to save their data into a
# single data object. Errors parsing the scripts are a bug in anaconda,
# so just raise an exception.
ksparser = AnacondaKSParser(ksdata, scriptClass=AnacondaInternalScript)
ksparser.readKickstartFromString(scripts, reset=False)
def runPostScripts(scripts):
postScripts = [s for s in scripts if s.type == KS_SCRIPT_POST]
if len(postScripts) == 0:
return
script_log.info("Running kickstart %%post script(s)")
for script in postScripts:
script.run(conf.target.system_root)
script_log.info("All kickstart %%post script(s) have been run")
def runPreScripts(scripts):
preScripts = [s for s in scripts if s.type == KS_SCRIPT_PRE]
if len(preScripts) == 0:
return
script_log.info("Running kickstart %%pre script(s)")
stdoutLog.info(_("Running pre-installation scripts"))
for script in preScripts:
script.run("/")
script_log.info("All kickstart %%pre script(s) have been run")
def runPreInstallScripts(scripts):
preInstallScripts = [s for s in scripts if s.type == KS_SCRIPT_PREINSTALL]
if len(preInstallScripts) == 0:
return
script_log.info("Running kickstart %%pre-install script(s)")
for script in preInstallScripts:
script.run("/")
script_log.info("All kickstart %%pre-install script(s) have been run")
def runTracebackScripts(scripts):
script_log.info("Running kickstart %%traceback script(s)")
for script in filter(lambda s: s.type == KS_SCRIPT_TRACEBACK, scripts):
script.run("/")
script_log.info("All kickstart %%traceback script(s) have been run")