diff --git a/anaconda.py b/anaconda.py index 7a289864bccc..bfcfb957dee2 100755 --- a/anaconda.py +++ b/anaconda.py @@ -388,7 +388,7 @@ def _earlyExceptionHandler(ty, value, traceback): log.info("Found a kickstart file: %s", kspath) # Run %pre scripts. - startup_utils.run_pre_scripts(kspath) + startup_utils.run_pre_scripts() # Parse the kickstart file. ksdata = startup_utils.parse_kickstart(kspath, strict_mode=opts.ksstrict) diff --git a/pyanaconda/installation.py b/pyanaconda/installation.py index 77dcfa49d7c1..a271f4047411 100644 --- a/pyanaconda/installation.py +++ b/pyanaconda/installation.py @@ -378,7 +378,7 @@ def _prepare_installation(self, payload, ksdata): ) pre_install_scripts.append(Task( "Run %pre-install scripts", - runPreInstallScripts, (ksdata.scripts,) + runPreInstallScripts, () )) installation_queue.append(pre_install_scripts) diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 33bd9b1333b9..6c436ec17433 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -38,17 +38,17 @@ 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.constants.objects import SCRIPTS +from pyanaconda.modules.common.constants.services import BOSS, RUNTIME 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.constants import KS_SCRIPT_POST, KS_SCRIPT_TRACEBACK 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.sections import NullSection, PreScriptSection, PostScriptSection, OnErrorScriptSection, TracebackScriptSection, Section from pykickstart.version import returnClassForVersion log = get_module_logger(__name__) @@ -259,6 +259,9 @@ def __init__(self, commandUpdates=None, dataUpdates=None): # The %packages section is handled by the DBus module. self.packages = UselessObject() + # The %pre, %pre-install sections are handled by the DBus module. + self.scripts = UselessObject() + def __str__(self): proxy = BOSS.get_proxy() modules = proxy.GenerateKickstart().strip() @@ -296,11 +299,11 @@ def handleCommand(self, lineno, args): 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="%pre")) + self.registerSection(UselessSection(self.handler, sectionOpen="%pre-install")) self.registerSection(UselessSection(self.handler, sectionOpen="%packages")) self.registerSection(UselessSection(self.handler, sectionOpen="%addon")) @@ -309,13 +312,9 @@ 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) + runPreScripts() def parseKickstart(handler, f, strict_mode=False): @@ -406,33 +405,18 @@ def runPostScripts(scripts): 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 runPreScripts(): + runtime_proxy = RUNTIME.get_proxy(SCRIPTS) + task = runtime_proxy.RunPreScriptsWithTask() -def runPreInstallScripts(scripts): - preInstallScripts = [s for s in scripts if s.type == KS_SCRIPT_PREINSTALL] + return task - if len(preInstallScripts) == 0: - return - - script_log.info("Running kickstart %%pre-install script(s)") - for script in preInstallScripts: - script.run("/") +def runPreInstallScripts(): + runtime_proxy = RUNTIME.get_proxy(SCRIPTS) + task = runtime_proxy.RunPreInstallScriptsWithTask() - script_log.info("All kickstart %%pre-install script(s) have been run") + return task def runTracebackScripts(scripts): diff --git a/pyanaconda/modules/common/constants/objects.py b/pyanaconda/modules/common/constants/objects.py index 2deb5890dcbe..29b79c21f2b5 100644 --- a/pyanaconda/modules/common/constants/objects.py +++ b/pyanaconda/modules/common/constants/objects.py @@ -28,6 +28,11 @@ basename="UserInterface" ) +SCRIPTS = DBusObjectIdentifier( + namespace=RUNTIME_NAMESPACE, + basename="ScriptsInterface" +) + # Storage objects. BOOTLOADER = DBusObjectIdentifier( diff --git a/pyanaconda/modules/common/structures/scripts.py b/pyanaconda/modules/common/structures/scripts.py new file mode 100644 index 000000000000..e5c0096c48ff --- /dev/null +++ b/pyanaconda/modules/common/structures/scripts.py @@ -0,0 +1,58 @@ +# +# DBus structures for the packages data. +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from dasbus.structure import DBusData +from dasbus.typing import * # pylint: disable=wildcard-import + +class Script(DBusData): + """ Structure for the script data. """ + + def __init__(self, _type: Int, script: Str, interp: Str, logfile: Str, errorOnFail: Bool, lineno: Int): + self.type = _type + self.script = script + self.interp = interp + self.logfile = logfile + self.errorOnFail = errorOnFail + self.lineno = lineno + + @property + def type(self) -> Int: + """ The type of the script. + + :return: The type of the script. + :rtype: Int + """ + return self.type + + @type.setter + def type(self, value: Int) -> None: + self.type = value + + @property + def script(self) -> Str: + """ The script. + + :return: The script. + :rtype: Str + """ + return self.script + + @script.setter + def script(self, value: Str) -> None: + self.script = value diff --git a/pyanaconda/modules/runtime/kickstart.py b/pyanaconda/modules/runtime/kickstart.py index caffe73d1be0..2f418f2c66b2 100644 --- a/pyanaconda/modules/runtime/kickstart.py +++ b/pyanaconda/modules/runtime/kickstart.py @@ -18,6 +18,7 @@ # Red Hat, Inc. # from pyanaconda.core.kickstart import KickstartSpecification, commands as COMMANDS +from pykickstart.sections import PreScriptSection class RuntimeKickstartSpecification(KickstartSpecification): @@ -34,3 +35,7 @@ class RuntimeKickstartSpecification(KickstartSpecification): "DriverDiskData": COMMANDS.DriverDiskData, "SshPwData": COMMANDS.SshPwData, } + + sections = { + "pre": PreScriptSection, + } diff --git a/pyanaconda/modules/runtime/runtime.py b/pyanaconda/modules/runtime/runtime.py index 569e4cccc9ad..1c092ff0d8d7 100755 --- a/pyanaconda/modules/runtime/runtime.py +++ b/pyanaconda/modules/runtime/runtime.py @@ -23,6 +23,7 @@ from pyanaconda.modules.runtime.kickstart import RuntimeKickstartSpecification from pyanaconda.modules.runtime.dracut_commands import DracutCommandsModule from pyanaconda.modules.runtime.user_interface import UIModule +from pyanaconda.modules.runtime.scripts import ScriptsModule from pyanaconda.modules.common.base import KickstartService from pyanaconda.modules.common.constants.services import RUNTIME from pyanaconda.modules.common.containers import TaskContainer @@ -51,6 +52,9 @@ def __init__(self): self._ui_module = UIModule() self._modules.add_module(self._ui_module) + self._scripts_module = ScriptsModule() + self._modules.add_module(self._scripts_module) + def publish(self): """Publish the module.""" TaskContainer.set_namespace(RUNTIME.namespace) diff --git a/pyanaconda/modules/runtime/scripts/Makefile.am b/pyanaconda/modules/runtime/scripts/Makefile.am new file mode 100644 index 000000000000..a25c798845f3 --- /dev/null +++ b/pyanaconda/modules/runtime/scripts/Makefile.am @@ -0,0 +1,21 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) +scriptsdir = $(pkgpyexecdir)/modules/runtime/scripts +scripts_PYTHON = $(srcdir)/*.py + +MAINTAINERCLEANFILES = Makefile.in diff --git a/pyanaconda/modules/runtime/scripts/__init__.py b/pyanaconda/modules/runtime/scripts/__init__.py new file mode 100644 index 000000000000..4f3b772bba62 --- /dev/null +++ b/pyanaconda/modules/runtime/scripts/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from pyanaconda.modules.runtime.scripts.scripts import ScriptsModule + +__all__ = ["ScriptsModule"] diff --git a/pyanaconda/modules/runtime/scripts/scripts.py b/pyanaconda/modules/runtime/scripts/scripts.py new file mode 100644 index 000000000000..5d4f117d9419 --- /dev/null +++ b/pyanaconda/modules/runtime/scripts/scripts.py @@ -0,0 +1,191 @@ +# +# The user interface module +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +import os +import sys +import tempfile +from pykickstart.constants import KS_SCRIPT_PRE, KS_SCRIPT_PREINSTALL + +from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core import util +from pyanaconda.core.constants import IPMI_ABORTED +from pyanaconda.core.dbus import DBus +from pyanaconda.errors import ScriptError, errorHandler +from pyanaconda.flags import flags +from pyanaconda.modules.common.base import KickstartBaseModule +from pyanaconda.modules.common.constants.objects import SCRIPTS +from pyanaconda.modules.common.structures.scripts import Script +from pyanaconda.modules.common.task import Task +from pyanaconda.modules.runtime.scripts.scripts_interface import ScriptsInterface + +# Set up the modules logger. +log = get_module_logger(__name__) + +__all__ = ["ScriptsModule"] + + +log = get_module_logger(__name__) + +class ScriptsModule(KickstartBaseModule): + """The scripts module. + + """ + def __init__(self): + super().__init__() + self._pre = None + self._pre_install = None + + def publish(self): + """Publish the module.""" + DBus.publish_object(SCRIPTS.object_path, ScriptsInterface(self)) + + def run_pre_script_with_task(self): + """Run the pre script""" + log.debug("Running pre script with task") + + # FIXME + # pylint: disable=no-member + task = RunScriptTask(self.pre.script) + return task + + def run_pre_install_script_with_task(self): + """Run the pre-installation script.""" + log.debug("Running pre-installation script with task") + + task = RunScriptTask(self.pre_install.script) + return task + + @property + def pre(self): + """The pre script.""" + return self._pre + + def set_pre(self, value): + """Set the pre script.""" + self._pre = Script.to_structure(value) + log.debug("Setting pre script to %s", value) + + @property + def pre_install(self): + """The pre-installation script.""" + return self.pre_install + + def set_pre_install(self, value): + """Set the pre-installation script.""" + self._pre_install = Script.to_structure(value) + log.debug("Setting pre-installation script to %s", value) + + def process_kickstart(self, data): + """Process the kickstart data.""" + if data.scripts.pre: + self.set_pre(data.scripts.pre) + + if data.scripts.pre_install: + self.set_pre_install(data.scripts.pre_install) + + def setup_kickstart(self, data): + """Set up the kickstart data.""" + if self.pre: + data.scripts.pre = self.pre + + if self.pre_install: + data.scripts.pre_install = self.pre_install + + +class RunScriptTask(Task): + """Runs script task.""" + + def __init__(self, script, inChroot=False, errorOnFail=True, logfile=None, lineno=None, chroot=None, interp="/bin/sh"): + """Create a new task. + + :param script: KS section script + :type script: str + :param inChroot: whether to run the script in the chroot + :type inChroot: bool + :param errorOnFail: whether to abort the installation if the script fails + :type errorOnFail: bool + :param logfile: the file to log the output to + :type logfile: str + """ + super().__init__() + self.chroot = chroot + self.errorOnFail = errorOnFail + self.inChroot = inChroot + self.interp = interp + self.lineno = lineno + self.logfile = logfile + self.script = script + + @property + def name(self): + return "Run script" + + def run(self): + """ Run the kickstart script + @param chroot directory path to chroot into before execution + """ + if self.inChroot: + scriptRoot = self.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(messages, "w") as fp: + rc = util.execWithRedirect(self.interp, ["/tmp/%s" % os.path.basename(path)], + stdout=fp, + root=scriptRoot) + + if rc != 0: + 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) + + + diff --git a/pyanaconda/modules/runtime/scripts/scripts_interface.py b/pyanaconda/modules/runtime/scripts/scripts_interface.py new file mode 100644 index 000000000000..accb20f7e5ad --- /dev/null +++ b/pyanaconda/modules/runtime/scripts/scripts_interface.py @@ -0,0 +1,43 @@ +# +# DBus interface for the scripts module +# +# Copyright (C) 2021 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from dasbus.server.interface import dbus_interface +from dasbus.typing import * # pylint: disable=wildcard-import + +from pyanaconda.modules.common.base import KickstartModuleInterfaceTemplate +from pyanaconda.modules.common.constants.objects import SCRIPTS +from pyanaconda.modules.common.containers import TaskContainer + +__all__ = ["ScriptsInterface"] + + +@dbus_interface(SCRIPTS.interface_name) +class ScriptsInterface(KickstartModuleInterfaceTemplate): + """DBus interface for the scripts module.""" + def RunPreScriptsWithTask(self) -> ObjPath: + """Run pre scripts with task.""" + return TaskContainer.to_object_path( + self.implementation.run_pre_scripts_with_task() + ) + + def RunPreInstallScriptsWithTask(self) -> ObjPath: + """Run pre install scripts with task.""" + return TaskContainer.to_object_path( + self.implementation.run_pre_install_scripts_with_task() + ) diff --git a/pyanaconda/startup_utils.py b/pyanaconda/startup_utils.py index efd5ce3686da..adf0fdb5a73a 100644 --- a/pyanaconda/startup_utils.py +++ b/pyanaconda/startup_utils.py @@ -399,14 +399,9 @@ def find_kickstart(options): return None -def run_pre_scripts(ks_path): - """Run %pre scripts. - - :param ks_path: a path to a kickstart file or None - """ - if ks_path is not None: - kickstart.preScriptPass(ks_path) - +def run_pre_scripts(): + """Run %pre scripts.""" + kickstart.runPreScripts() def parse_kickstart(ks_path, strict_mode=False): """Parse the given kickstart file. diff --git a/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_runtime.py b/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_runtime.py index 0d2c775d5138..dcd0e0375520 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_runtime.py +++ b/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_runtime.py @@ -81,3 +81,17 @@ def test_kickstart_driverdisk(self): ks_in = "driverdisk --source=nfs:host:/path/to/img\n" ks_out = "driverdisk --source=nfs:host:/path/to/img\n" self._test_kickstart(ks_in, ks_out) + + def test_kickstart_pre_script(self): + """Test saving the pre script via kickstart.""" + ks_in = dedent(""" + %pre + echo "Hello, world!" + %end + """) + ks_out = dedent(""" + %pre + echo "Hello, world!" + %end + """) + self._test_kickstart(ks_in, ks_out) diff --git a/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_scripts.py b/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_scripts.py new file mode 100644 index 000000000000..5a8de27dc9e7 --- /dev/null +++ b/tests/unit_tests/pyanaconda_tests/modules/runtime/test_module_scripts.py @@ -0,0 +1,61 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +import unittest + +from pyanaconda.modules.common.constants.objects import SCRIPTS +from pyanaconda.modules.runtime.scripts import ScriptsModule +from pyanaconda.modules.runtime.scripts.scripts import RunScriptTask +from pyanaconda.modules.runtime.scripts.scripts_interface import ScriptsInterface +from tests.unit_tests.pyanaconda_tests import check_kickstart_interface, patch_dbus_publish_object + +class ScriptsInterfaceTestCase(unittest.TestCase): + """ Test Scripts DBus interface for the runtime module.""" + + def setUp(self): + self.module = ScriptsModule() + self.interface = ScriptsInterface(self.module) + + def _check_dbus_property(self, *args, **kwargs): + check_dbus_property( + SCRIPTS, + self.interface, + *args, **kwargs + ) + + def _test_kickstart(self, ks_in, ks_out): + check_kickstart_interface(self.interface, ks_in, ks_out) + + def test_kickstart_set_pre_script(self): + """Test setting pre script via kickstart.""" + ks_in = """ +%pre +echo PRE +%end +""" + ks_out = """ +%pre +echo PRE +%end +""" + self._test_kickstart(ks_in, ks_out) + assert self.interface.pre.script == ks_out + + @patch_dbus_publish_object + def test_run_pre_script_with_task(self, publisher): + """Test RunPreScriptsWithTask method.""" + task_path = self.interface.RunPreScriptsWithTask() + check_task_creation(taks_path, publisher, RunScriptTask)