diff --git a/configure.ac b/configure.ac index ed92982e1a42..54ea0e1faa74 100644 --- a/configure.ac +++ b/configure.ac @@ -180,6 +180,7 @@ AC_CONFIG_FILES([Makefile pyanaconda/modules/payloads/source/url/Makefile pyanaconda/modules/runtime/Makefile pyanaconda/modules/runtime/dracut_commands/Makefile + pyanaconda/modules/runtime/scripts/Makefile pyanaconda/modules/runtime/user_interface/Makefile pyanaconda/modules/storage/Makefile pyanaconda/modules/storage/bootloader/Makefile diff --git a/pyanaconda/core/kickstart/scripts.py b/pyanaconda/core/kickstart/scripts.py new file mode 100644 index 000000000000..68b0327496d2 --- /dev/null +++ b/pyanaconda/core/kickstart/scripts.py @@ -0,0 +1,79 @@ +# +# Commond utilities for working with scripts +# +# 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 tempfile +from pyanaconda.core import util +from pyanaconda.core.path import open_with_perm + +from pyanaconda.anaconda_loggers import get_module_logger +log = get_module_logger(__name__) + +script_log = log.getChild("script") + + +__all__ = ["run_script"] + + +def run_script(script, chroot): + """ Run the 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 + # TODORV return + @param chroot directory path to chroot into before execution + """ + if script.inChroot: + scriptRoot = chroot + else: + scriptRoot = "/" + + (fd, path) = tempfile.mkstemp("", "ks-script-", scriptRoot + "/tmp") + + os.write(fd, script.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 script.logfile: + if script.inChroot: + messages = "%s/%s" % (scriptRoot, script.logfile) + else: + messages = script.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(script.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, script.lineno) + + return rc, messages diff --git a/pyanaconda/core/kickstart/specification.py b/pyanaconda/core/kickstart/specification.py index e261ec2a01de..78a5eba94ab5 100644 --- a/pyanaconda/core/kickstart/specification.py +++ b/pyanaconda/core/kickstart/specification.py @@ -91,6 +91,8 @@ def __init__(self, specification): for name, data in specification.addons.items(): self.registerAddonData(name, data) + self.scripts = [] + def registerSectionData(self, name, data): """Register data used by a section.""" obj = data() diff --git a/pyanaconda/installation.py b/pyanaconda/installation.py index 77dcfa49d7c1..9922701c6500 100644 --- a/pyanaconda/installation.py +++ b/pyanaconda/installation.py @@ -23,9 +23,9 @@ CATEGORY_BOOTLOADER, CATEGORY_ENVIRONMENT, CATEGORY_STORAGE, CATEGORY_SOFTWARE from pyanaconda.modules.boss.install_manager.installation_category_interface \ import CategoryReportTaskInterface -from pyanaconda.modules.common.constants.objects import BOOTLOADER, SNAPSHOT, FIREWALL +from pyanaconda.modules.common.constants.objects import BOOTLOADER, SNAPSHOT, FIREWALL, SCRIPTS from pyanaconda.modules.common.constants.services import STORAGE, USERS, SERVICES, NETWORK, \ - SECURITY, LOCALIZATION, TIMEZONE, BOSS, SUBSCRIPTION + SECURITY, LOCALIZATION, TIMEZONE, BOSS, SUBSCRIPTION, RUNTIME from pyanaconda.modules.common.task import sync_run_task, Task as InstallationTask from pyanaconda.modules.common.util import is_module_available from pyanaconda import flags @@ -38,7 +38,8 @@ from pyanaconda.kickstart import runPostScripts, runPreInstallScripts from pyanaconda.kexec import setup_kexec from pyanaconda.installation_tasks import Task, TaskQueue, DBusTask -from pykickstart.constants import SNAPSHOT_WHEN_POST_INSTALL +from pykickstart.constants import (SNAPSHOT_WHEN_POST_INSTALL, KS_SCRIPT_PREINSTALL, + KS_SCRIPT_POST) from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) @@ -291,6 +292,10 @@ def run_generate_initramfs(): runPostScripts, (ksdata.scripts,) )) + scripts_proxy = RUNTIME.get_proxy(SCRIPTS) + post_scripts.append_dbus_tasks(RUNTIME, [ + scripts_proxy.RunScriptsWithTask(KS_SCRIPT_POST) + ]) configuration_queue.append(post_scripts) boss_proxy = BOSS.get_proxy() @@ -380,6 +385,10 @@ def _prepare_installation(self, payload, ksdata): "Run %pre-install scripts", runPreInstallScripts, (ksdata.scripts,) )) + scripts_proxy = RUNTIME.get_proxy(SCRIPTS) + pre_install_scripts.append_dbus_tasks(RUNTIME, [ + scripts_proxy.RunScriptsWithTask(KS_SCRIPT_PREINSTALL) + ]) installation_queue.append(pre_install_scripts) # Do various pre-installation tasks diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 068487393355..e1cf678b13fe 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -30,6 +30,7 @@ from pyanaconda.anaconda_loggers import get_module_logger, get_stdout_logger from pyanaconda.core import util +from pyanaconda.core.kickstart.scripts import run_script from pyanaconda.core.path import open_with_perm from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.kickstart import VERSION, commands as COMMANDS @@ -72,63 +73,19 @@ def check_kickstart_error(): 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) + rc, log_file = run_script(self, chroot) + if self.errorOnFail and rc != 0: + err = "" + with open(log_file, "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): diff --git a/pyanaconda/modules/common/constants/objects.py b/pyanaconda/modules/common/constants/objects.py index 2deb5890dcbe..6eb81effe9a4 100644 --- a/pyanaconda/modules/common/constants/objects.py +++ b/pyanaconda/modules/common/constants/objects.py @@ -21,7 +21,12 @@ PARTITIONING_NAMESPACE, DEVICE_TREE_NAMESPACE, \ RHSM_NAMESPACE, RUNTIME_NAMESPACE -# Boss objects. +# Runtime objects + +SCRIPTS = DBusObjectIdentifier( + namespace=RUNTIME_NAMESPACE, + basename="Scripts" +) USER_INTERFACE = DBusObjectIdentifier( namespace=RUNTIME_NAMESPACE, diff --git a/pyanaconda/modules/runtime/Makefile.am b/pyanaconda/modules/runtime/Makefile.am index 5523d644dd39..5bbcfeec7c51 100644 --- a/pyanaconda/modules/runtime/Makefile.am +++ b/pyanaconda/modules/runtime/Makefile.am @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -SUBDIRS = dracut_commands user_interface +SUBDIRS = dracut_commands scripts user_interface pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) runtimedir = $(pkgpyexecdir)/modules/runtime diff --git a/pyanaconda/modules/runtime/kickstart.py b/pyanaconda/modules/runtime/kickstart.py index 2f7627820ddf..ee02a52fea6e 100644 --- a/pyanaconda/modules/runtime/kickstart.py +++ b/pyanaconda/modules/runtime/kickstart.py @@ -17,6 +17,9 @@ # License and may only be used or replicated with the express permission of # Red Hat, Inc. # +from pykickstart.parser import Script +from pykickstart.sections import PreInstallScriptSection, PostScriptSection + from pyanaconda.core.kickstart import KickstartSpecification, commands as COMMANDS @@ -41,3 +44,8 @@ class RuntimeKickstartSpecification(KickstartSpecification): "DriverDiskData": COMMANDS.DriverDiskData, "SshPwData": COMMANDS.SshPwData, } + + sections = { + "pre-install": lambda handler: PreInstallScriptSection(handler, dataObj=Script), + "post": lambda handler: PostScriptSection(handler, dataObj=Script), + } diff --git a/pyanaconda/modules/runtime/runtime.py b/pyanaconda/modules/runtime/runtime.py index ca1d59d19877..bcd9562d6a91 100755 --- a/pyanaconda/modules/runtime/runtime.py +++ b/pyanaconda/modules/runtime/runtime.py @@ -25,6 +25,7 @@ from pyanaconda.modules.runtime.runtime_interface import RuntimeInterface from pyanaconda.modules.runtime.kickstart import RuntimeKickstartSpecification from pyanaconda.modules.runtime.dracut_commands import DracutCommandsModule +from pyanaconda.modules.runtime.scripts import ScriptsModule from pyanaconda.modules.runtime.user_interface import UIModule from pyanaconda.modules.common.base import KickstartService from pyanaconda.modules.common.constants.services import RUNTIME @@ -51,6 +52,9 @@ def __init__(self): self._dracut_module = DracutCommandsModule() self._modules.add_module(self._dracut_module) + self._scripts_module = ScriptsModule() + self._modules.add_module(self._scripts_module) + self._ui_module = UIModule() self._modules.add_module(self._ui_module) diff --git a/pyanaconda/modules/runtime/scripts/Makefile.am b/pyanaconda/modules/runtime/scripts/Makefile.am new file mode 100644 index 000000000000..c46bd8662fdf --- /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 +dist_scripts_DATA = $(wildcard $(srcdir)/*.py) + +MAINTAINERCLEANFILES = Makefile.in \ No newline at end of file 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..1d1e72c04005 --- /dev/null +++ b/pyanaconda/modules/runtime/scripts/scripts.py @@ -0,0 +1,98 @@ +# +# 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. +# +# Set up the modules logger. +import sys + +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.core.kickstart.scripts import run_script +from pyanaconda.errors import errorHandler, ScriptError +from pyanaconda.flags import flags +from pyanaconda.modules.common.base import KickstartBaseModule +from pyanaconda.modules.common.constants.objects import SCRIPTS +from pyanaconda.modules.common.task import Task +from pyanaconda.modules.runtime.scripts.scripts_interface import ScriptsInterface + +log = get_module_logger(__name__) + +__all__ = ["ScriptsModule"] + +class RunScriptsTask(Task): + """Task for running scripts.""" + + def __init__(self, script_type, scripts): + """Create a new task. + :param script_type: type of scripts to be run + :type script_type: int + :param scripts: list of scripts + :type scripts: list(Script) + """ + super().__init__() + self._script_type = script_type + self._scripts = scripts + + @property + def name(self): + return "Run scripts" + + def run(self): + for script in self._scripts: + if script.type == self._script_type: + rc, log_file = run_script(script, "/") + if script.errorOnFail and rc != 0: + err = "" + with open(log_file, "r") as fp: + err = "".join(fp.readlines()) + + # Show error dialog even for non-interactive + flags.ksprompt = True + + errorHandler.cb(ScriptError(script.lineno, err)) + util.ipmi_report(IPMI_ABORTED) + sys.exit(0) + + + +class ScriptsModule(KickstartBaseModule): + def __init__(self): + super().__init__() + self._scripts = [] + + def publish(self): + """Publish the module.""" + DBus.publish_object(SCRIPTS.object_path, ScriptsInterface(self)) + + def process_kickstart(self, data): + log.debug("DDDDD process_kickstart %s", data.scripts) + self._scripts = data.scripts + + def setup_kickstart(self, data): + log.debug("DDDDD setup_kickstart %s", data) + data.scripts = self._scripts + + def run_scripts_with_task(self, script_type): + """Run all scripts of given type sequentially.""" + log.debug("DDDDD running %s scripts with task", script_type) + return RunScriptsTask( + script_type=script_type, + scripts=self._scripts + ) diff --git a/pyanaconda/modules/runtime/scripts/scripts_interface.py b/pyanaconda/modules/runtime/scripts/scripts_interface.py new file mode 100644 index 000000000000..8761843fefa8 --- /dev/null +++ b/pyanaconda/modules/runtime/scripts/scripts_interface.py @@ -0,0 +1,48 @@ +# +# 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 RunScriptsWithTask(self, script_type: Int) -> ObjPath: + """Run all scripts of given type sequentially with task. + The types of scripts: + kickstart scripts: + KS_SCRIPT_PRE = 0 + KS_SCRIPT_POST = 1 + KS_SCRIPT_TRACEBACK = 2 + KS_SCRIPT_PREINSTALL = 3 + KS_SCRIPT_ONERROR = 4 + :param script_type: Type of scripts to be run. + :return: a DBus path of the task + """ + return TaskContainer.to_object_path( + self.implementation.run_scripts_with_task(script_type) + )