From 824f0d4431d030458d410a1b3097071697f2fa5a Mon Sep 17 00:00:00 2001 From: Ben Morrice <ben.morrice@cern.ch> Date: Wed, 15 Jun 2022 14:25:40 +0200 Subject: [PATCH] Add support for CS9 (including moving to anaconda dbus calls) --- cern-anaconda-addon.spec | 13 +- .../categories/firstboot.py | 19 -- src/cern_customizations/constants.py | 16 +- .../gui/spokes/cern_firstboot.py | 53 +-- src/cern_customizations/ks/__init__.py | 0 src/cern_customizations/ks/cern.py | 314 ------------------ .../{categories => service}/__init__.py | 0 src/cern_customizations/service/__main__.py | 6 + src/cern_customizations/service/cern.py | 111 +++++++ .../service/cern_interface.py | 67 ++++ .../service/installation.py | 165 +++++++++ src/cern_customizations/service/kickstart.py | 194 +++++++++++ ...ct.Anaconda.Addons.CernCustomizations.conf | 13 + ...Anaconda.Addons.CernCustomizations.service | 4 + 14 files changed, 619 insertions(+), 356 deletions(-) delete mode 100644 src/cern_customizations/categories/firstboot.py delete mode 100644 src/cern_customizations/ks/__init__.py delete mode 100644 src/cern_customizations/ks/cern.py rename src/cern_customizations/{categories => service}/__init__.py (100%) create mode 100644 src/cern_customizations/service/__main__.py create mode 100644 src/cern_customizations/service/cern.py create mode 100644 src/cern_customizations/service/cern_interface.py create mode 100644 src/cern_customizations/service/installation.py create mode 100644 src/cern_customizations/service/kickstart.py create mode 100644 src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.conf create mode 100644 src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.service diff --git a/cern-anaconda-addon.spec b/cern-anaconda-addon.spec index cd2132f..81cb57a 100644 --- a/cern-anaconda-addon.spec +++ b/cern-anaconda-addon.spec @@ -1,6 +1,6 @@ Name: cern-anaconda-addon -Version: 1.8 -Release: 5%{?dist} +Version: 1.9 +Release: 1%{?dist} Summary: CERN configuration anaconda addon Group: CERN/Utilities @@ -21,16 +21,25 @@ CERN configuration anaconda addon. install -d %{buildroot}/%{_datadir}/anaconda/addons/ install -d %{buildroot}/%{_datadir}/icons/hicolor/scalable/apps/ +install -d %{buildroot}/%{_datadir}/anaconda/dbus/confs/ +install -d %{buildroot}/%{_datadir}/anaconda/dbus/services/ cp -ra cern_customizations %{buildroot}/%{_datadir}/anaconda/addons/ install -p -m 644 data/cern.svg %{buildroot}/%{_datadir}/icons/hicolor/scalable/apps/ +install -p -m 644 data/org.fedoraproject.Anaconda.Addons.CernCustomizations.conf %{buildroot}/%{_datadir}/anaconda/dbus/confs/ +install -p -m 644 data/org.fedoraproject.Anaconda.Addons.CernCustomizations.service %{buildroot}/%{_datadir}/anaconda/dbus/services/ %files %{_datadir}/anaconda/addons/cern_customizations %{_datadir}/icons/hicolor/scalable/apps/cern.svg +%{_datadir}/anaconda/dbus/confs/org.fedoraproject.Anaconda.Addons.CernCustomizations.conf +%{_datadir}/anaconda/dbus/services/org.fedoraproject.Anaconda.Addons.CernCustomizations.service %doc LICENSE README.md %changelog +* Wed Jun 15 2022 Ben Morrice <ben.morrice@cern.ch> - 1.9-1 +- Add a D-Bus service to perform the work for the addon + * Tue Mar 29 2022 Ben Morrice <ben.morrice@cern.ch> - 1.8-5 - Add nscd as a default module for the 8 family diff --git a/src/cern_customizations/categories/firstboot.py b/src/cern_customizations/categories/firstboot.py deleted file mode 100644 index d1ffc27..0000000 --- a/src/cern_customizations/categories/firstboot.py +++ /dev/null @@ -1,19 +0,0 @@ -"""CERN Firstboot category module""" -from pyanaconda.ui.categories import SpokeCategory - -N_ = lambda x: x - -__all__ = ["CernCategory"] - -class CernCategory(SpokeCategory): - """ - Class for the Firstboot Cern category. Category groups related spokes - together. Both logically and visually (creates a box on a hub). It - references a class of the hub it is supposed to be placed on. On the - other hand spokes reference a class of the category they should be - included in. - - """ - displayOnHubGUI = "SummaryHub" - displayOnHubTUI = "SummaryHub" - title = N_("CERN") \ No newline at end of file diff --git a/src/cern_customizations/constants.py b/src/cern_customizations/constants.py index 9591b4b..38a8bd2 100644 --- a/src/cern_customizations/constants.py +++ b/src/cern_customizations/constants.py @@ -1,7 +1,7 @@ release=open('/etc/redhat-release','r').read().split(' ')[3].split('.')[0].rstrip() if release == '7': LOCMAP_CORE_MODULES = ["ssh", "sendmail", "sudo", "ntp", "kerberos", "lpadmin", "nscd"] -elif release == '8': +elif release == '8' or release == '9': LOCMAP_CORE_MODULES = ["ssh", "sudo", "chrony", "kerberos", "lpadmin", "postfix", "nscd"] # unsupported future distribution else: @@ -25,3 +25,17 @@ GNOME_INIT_DEFAULT = False EOS_IS_DEFAULT = False AUTOUPDATE_IS_DEFAULT = True AUTOUPDATEBOOT_IS_DEFAULT = False + + +from dasbus.identifier import DBusServiceIdentifier +from pyanaconda.core.dbus import DBus +from pyanaconda.modules.common.constants.namespaces import ADDONS_NAMESPACE + +# These define location of the addon's service on D-Bus. See also the data/*.conf file. + +CERN_NAMESPACE = (*ADDONS_NAMESPACE, "CernCustomizations") + +CERN = DBusServiceIdentifier( + namespace=CERN_NAMESPACE, + message_bus=DBus +) diff --git a/src/cern_customizations/gui/spokes/cern_firstboot.py b/src/cern_customizations/gui/spokes/cern_firstboot.py index 359460f..7b41b54 100644 --- a/src/cern_customizations/gui/spokes/cern_firstboot.py +++ b/src/cern_customizations/gui/spokes/cern_firstboot.py @@ -1,14 +1,25 @@ """Module with the CernSpoke class.""" import logging -from cern_customizations.categories.firstboot import CernCategory +from cern_customizations.constants import CERN from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.spokes import NormalSpoke +from pyanaconda.ui.categories import SpokeCategory from pyanaconda.ui.common import FirstbootOnlySpokeMixIn from pyanaconda.ui.common import FirstbootSpokeMixIn from cern_customizations.i18n import _, N_ +class CernCategory(SpokeCategory): + + @staticmethod + def get_title(): + return _("CERN") + + @staticmethod + def get_sort_order(): + return 100 + __all__ = ["CernSpoke"] class CernSpoke(FirstbootOnlySpokeMixIn, NormalSpoke): @@ -30,6 +41,7 @@ class CernSpoke(FirstbootOnlySpokeMixIn, NormalSpoke): def __init__(self, data, storage, payload): NormalSpoke.__init__(self, data, storage, payload) + self._cern_customizations_module = CERN.get_proxy() def initialize(self): NormalSpoke.initialize(self) @@ -45,7 +57,7 @@ class CernSpoke(FirstbootOnlySpokeMixIn, NormalSpoke): pass def apply(self): - self.data.addons.cern_customizations.configured = True + self._cern_customizations_module.SetConfigured(True) def execute(self): pass @@ -60,14 +72,14 @@ class CernSpoke(FirstbootOnlySpokeMixIn, NormalSpoke): @property def mandatory(self): - configured = self.data.addons.cern_customizations.configured + configured = self._cern_customizations_module.Configured if configured: return False else: return True @property def status(self): - configured = self.data.addons.cern_customizations.configured + configured = self._cern_customizations_module.Configured if configured: return _("Addon is configured\n\n ** Note **\n\n When clicking:\n'FINISH CONFIGURATION'\nPlease allow time for the customizations to be configured") @@ -77,29 +89,30 @@ class CernSpoke(FirstbootOnlySpokeMixIn, NormalSpoke): ### handlers ### def on_check_box_toggled(self, *args): if self._cern.get_active(): - self.data.addons.cern_customizations.afs = self._afs.get_active() - #self.data.addons.cern_customizations.eos = self._eos.get_active() - self.data.addons.cern_customizations.cvmfs = self._cvmfs.get_active() + self._cern_customizations_module.SetAfs(self._afs.get_active()) + #self._cern_customozations_module.eos = self._eos.get_active() + self._cern_customizations_module.SetCvmfs(self._cvmfs.get_active()) else: - self.data.addons.cern_customizations.afs = False - #self.data.addons.cern_customizations.eos = False - self.data.addons.cern_customizations.cvmfs = False + self._cern_customizations_module.SetAfs(False) + self._cern_customizations_module.SetCvmfs(False) + #self._cern_customizations_module.eos = False def on_radio_button_toggled(self, *args): if self._cern.get_active(): - self.data.addons.cern_customizations.run = True - self.data.addons.cern_customizations.autoupdate = self._cern_autoupdate.get_active() - self.data.addons.cern_customizations.afs = self._afs.get_active() - #self.data.addons.cern_customizations.eos = self._eos.get_active() - self.data.addons.cern_customizations.cvmfs = self._cvmfs.get_active() + self._cern_customizations_module.SetRun(True) + self._cern_customizations_module.SetAutoUpdate(self._cern_autoupdate.get_active()) + self._cern_customizations_module.SetAfs(self._afs.get_active()) + #self._cern_customizations_module.eos = self._eos.get_active() + self._cern_customizations_module.SetCvmfs(self._cvmfs.get_active()) if self._not_cern.get_active(): - self.data.addons.cern_customizations.run = False - self.data.addons.cern_customizations.autoupdate = self._not_cern_autoupdate.get_active() - self.data.addons.cern_customizations.afs = False - #self.data.addons.cern_customizations.eos = False - self.data.addons.cern_customizations.cvmfs = False + self._cern_customizations_module.SetRun(False) + self._cern_customizations_module.SetAutoUpdate(False) + self._cern_customizations_module.SetAfs(False) + + #self._cern_customizations_module.eos = False + self._cern_customizations_module.SetCvmfs(False) def on_entry_icon_clicked(self, entry, *args): """Handler for the textEntry's "icon-release" signal.""" diff --git a/src/cern_customizations/ks/__init__.py b/src/cern_customizations/ks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/cern_customizations/ks/cern.py b/src/cern_customizations/ks/cern.py deleted file mode 100644 index 8d7c402..0000000 --- a/src/cern_customizations/ks/cern.py +++ /dev/null @@ -1,314 +0,0 @@ - -"""Module with the CernData class.""" - -import os.path -import subprocess -import sys - -from pyanaconda.addons import AddonData - -from inspect import signature, Parameter -AddonDataSetupArgs=[] -for x, p in signature(AddonData.setup).parameters.items(): - if p.default == Parameter.empty and p.kind != Parameter.VAR_POSITIONAL: - AddonDataSetupArgs.append(x) - -# C8 -if (sys.version_info > (3, 0)): - try: - from pyanaconda.core.configuration.anaconda import conf - system_root = conf.target.system_root - # older versions - except: - from pyanaconda.core.util import getSysroot - system_root = getSysroot() -# CC7 -else: - from pyanaconda.iutil import getSysroot - system_root = getSysroot() -from cern_customizations.constants import SYSCONFIG_FILE_PATH, \ - AFS_IS_DEFAULT, EOS_IS_DEFAULT, CVMFS_IS_DEFAULT, \ - AUTOUPDATE_IS_DEFAULT, RUN_IS_DEFAULT, LOCMAP_BIN, \ - SYSTEMCTL_BIN, SYSCONFIG_YUM_PATH, \ - LOCMAP_CORE_MODULES, AUTOUPDATEBOOT_IS_DEFAULT, \ - GNOME_INIT_DEFAULT, GDMCONFIG_FILE_PATH, \ - LOCMAP_SYSTEMD_UNIT, XDGCONFIG_FILE_PATH - -from pykickstart.options import KSOptionParser -from pykickstart.errors import KickstartParseError, formatErrorMsg - -from pykickstart.version import versionToLongString, RHEL8 - -# export CernData class to prevent Anaconda's collect method from taking -# # AddonData class instead of the CernData class -# # :see: pyanaconda.kickstart.AnacondaKSHandler.__init__ - -__all__ = ["CernData"] - - -class CernData(AddonData): - """ - Class parsing and storing data for the CERN addon. - - :see: pyanaconda.addons.AddonData - """ - def __init__(self, name): - """ - Initialisation - - :param name: name of the addon - :type name: str - """ - AddonData.__init__(self, name) - self.run = RUN_IS_DEFAULT - self.afs = AFS_IS_DEFAULT - self.autoupdate = AUTOUPDATE_IS_DEFAULT - self.autoupdateboot = AUTOUPDATEBOOT_IS_DEFAULT - #self.eos = EOS_IS_DEFAULT - self.cvmfs = CVMFS_IS_DEFAULT - self.gnomeinit = GNOME_INIT_DEFAULT - self.configured = False - - def __str__(self): - """ - What should end up in the resulting kickstart file. - """ - addon_str = "%%addon %s " % self.name - if self.run: - addon_str += " --run-locmap" - if self.afs: - addon_str += " --afs-client" - if self.cvmfs: - addon_str += " --cvmfs-client" - #if self.eos: - # addon_str += " --eos-client" - if self.autoupdate: - addon_str += " --auto-update" - if self.autoupdateboot: - addon_str += " --auto-update-boot" - if self.gnomeinit: - addon_str += " --gnome-initial-setup" - addon_str += "\n%end\n" - return addon_str - - def handle_header(self, lineno, args): - """ - The handle_header method is called to parse additional arguments in the - %addon section line. - args is a list of all the arguments following the addon ID. For - example, for the line: - %addon org_fedora_hello_world --reverse --arg2="example" - handle_header will be called with args=['--reverse', '--arg2="example"'] - - :param lineno: the current line number in the kickstart file - :type lineno: int - :param args: the list of arguments from the %addon line - :type args: list - """ - - opc = KSOptionParser(version=RHEL8, description='CERN addon',prog='CERN') - opc.add_argument("--afs-client", action="store_true", default=True, - dest="afs", help="Install and configure Openafs client", - version=RHEL8) - - opc.add_argument("--auto-update", action="store_true", default=True, - dest="autoupdate", help="Enable automatic system updates", - version=RHEL8) - - opc.add_argument("--auto-update-boot", action="store_true", default=False, - dest="autoupdateboot", help="Enable automatic system updates at boot", - version=RHEL8) - - opc.add_argument("--cvmfs-client", action="store_true", default=False, - dest="cvmfs", help="Install and configure Fuse CVMFS client", - version=RHEL8) - - opc.add_argument("--gnome-initial-setup", action="store_true", default=False, - dest="gnomeinit", help="Run Gnome initial setup after first login", - version=RHEL8) - - opc.add_argument("--run-locmap", action="store_true", default=True, - dest="run", help="Run locmap at firstboot", - version=RHEL8) - opts = opc.parse_args(args=args, lineno=lineno) - - self.afs = opts.afs - self.run = opts.run - self.cvmfs = opts.cvmfs - self.autoupdate = opts.autoupdate - self.autoupdateboot = opts.autoupdateboot - self.gnomeinit = opts.gnomeinit - self.configured = True - - def handle_line(self, line): - """ - The handle_line method that is called with every line from this addon's - %addon section of the kickstart file. - - :param line: a single line from the %addon section - :type line: str - """ - pass - - def finalize(self): - """ - The finalize method that is called when the end of the %addon section - (i.e. the %end line) is processed. An addon should check if it has all - required data. If not, it may handle the case quietly or it may raise - the KickstartValueError exception. - """ - pass - - def setup(AddonDataSetupArgs): - """ - The setup method that should make changes to the runtime environment - according to the data stored in this object. - - :param storage: object storing storage-related information - (disks, partitioning, bootloader, etc.) - :type storage: blivet.Blivet instance - :param ksdata: data parsed from the kickstart file and set in the - installation process - :type ksdata: pykickstart.base.BaseHandler instance - :param instclass: distribution-specific information - :type instclass: pyanaconda.installclass.BaseInstallClass - :param payload: object managing packages and environment groups - for the installation - :type payload: any class inherited from the pyanaconda.packaging.Payload - class - """ - pass - - @property - def mandatory(self): - """ - The mandatory property that tells whether the spoke is mandatory to be - completed to continue in the installation process. - - :rtype: bool - - """ - - # this is a mandatory spoke, user must check it - return True - - def execute(self, storage, ksdata, users, payload): - - """ - The execute method that should make changes to the installed system. It - is called only once in the post-install setup phase. - - :see: setup - :param users: information about created users - :type users: pyanaconda.users.Users instance - """ - - yumupdate_file = os.path.normpath(system_root + SYSCONFIG_YUM_PATH) - - # Data is persisted in /root/initial-setup-ks.cfg - sysconfig_file = os.path.normpath(system_root + SYSCONFIG_FILE_PATH) - with open(sysconfig_file, "w") as fobj: - fobj.write("LOCMAP_FIRSTBOOT_START=%s\n" % self.run) - # For compatibility but can be dropped in >= 7.5 - fobj.write("LOCMAP_FIRSTBOOT_STARTAFS=%s\n" % self.afs) - #fobj.write("LOCMAP_FIRSTBOOT_EOSCLIENT=%s\n" % self.eos) - fobj.write("LOCMAP_FIRSTBOOT_CVMFS=%s\n" % self.cvmfs) - fobj.write("LOCMAP_FIRSTBOOT_UPDATE=%s\n" % self.autoupdate) - fobj.close() - - # Allow to disable gnome-initial-setup - gdmconfig_file = os.path.normpath(system_root + GDMCONFIG_FILE_PATH) - xdgconfig_file = os.path.normpath(system_root + XDGCONFIG_FILE_PATH) - if not self.gnomeinit: - # C8 - if (sys.version_info > (3, 0)): - from configparser import SafeConfigParser, NoOptionError, NoSectionError - # CC7 - else: - from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError - config = SafeConfigParser() - config.optionxform = str - config.read(gdmconfig_file) - try: - config.set('daemon', 'InitialSetupEnable', 'False') - with open(gdmconfig_file, "w") as fobj: - config.write(fobj) - fobj.close() - except (NoOptionError,NoSectionError): - print("Unexpected error:", sys.exc_info()[0]) - - configxdg = SafeConfigParser() - configxdg.optionxform = str - configxdg.read(xdgconfig_file) - try: - configxdg.set('Desktop Entry', 'X-GNOME-Autostart-enabled', 'false') - with open(xdgconfig_file, "w") as fobj: - configxdg.write(fobj) - fobj.close() - except (NoOptionError,NoSectionError): - print("Unexpected error:", sys.exc_info()[0]) - - # we will need this for later - release=open('/etc/redhat-release','r').read().split(' ')[3].split('.')[0].rstrip() - - # Configure yum-autoupdate - if self.autoupdate: - if release == '8': - subprocess.call(["dnf", "-y", "install", "yum-autoupdate"]) - else: - yumautoupdate_installed=os.path.isfile('/usr/sbin/yum-autoupdate') - if yumautoupdate_installed == False: - subprocess.call(["yum", "-y", "install", "yum-autoupdate"]) - subprocess.call([SYSTEMCTL_BIN, "enable", "yum-autoupdate"]) - try: - with open(yumupdate_file, "a") as fobj: - fobj.write("\n# initial-setup [cern-anaconda-cern] values\n") - fobj.write("YUMUPDATE=%d\n" % int(self.autoupdate)) - fobj.write("YUMONBOOT=%d\n" % int(self.autoupdateboot)) - fobj.close() - except IOError: - print("I/O Error couldn't write %s" % (yumupdate_file)) - except: - print("Unexpected error:", sys.exc_info()[0]) - else: - # we use call rather than check_call as we don't care if this fails - # due to yumautoupdate not being installed - if release == '8': - subprocess.call([SYSTEMCTL_BIN, "disable", "dnf-automatic-install.timer"]) - else: - subprocess.call([SYSTEMCTL_BIN, "disable", "yum-autoupdate"]) - - # locmap may not be installed - force install if it's not - locmap_installed=os.path.isfile('/usr/bin/locmap') - if locmap_installed == False: - # locmap does not reside in CERN repo on 8, install locmap-release - if release == '8': - subprocess.call(["dnf", "-y", "install", "locmap-release"]) - # 'yum' will work for both 7 and 8 - subprocess.call(["yum", "-y", "install", "locmap"]) - - # the above call to install locmap could have failed if the host is - # outside of CERN... - locmap_installed=os.path.isfile('/usr/bin/locmap') - if locmap_installed == True: - # Enable locmap core modules by default - for module in LOCMAP_CORE_MODULES: - subprocess.call([LOCMAP_BIN, "--enable", module]) - - # Configure and run locmap only if not disabled - if self.run: - subprocess.call([SYSTEMCTL_BIN, "enable", LOCMAP_SYSTEMD_UNIT]) - try: - if self.afs: - subprocess.check_call([LOCMAP_BIN, "--enable", "afs"]) - else: - subprocess.check_call([LOCMAP_BIN, "--disable", "afs"]) - if self.cvmfs: - subprocess.check_call([LOCMAP_BIN, "--enable", "cvmfs"]) - else: - subprocess.check_call([LOCMAP_BIN, "--disable", "cvmfs"]) - except subprocess.CalledProcessError: - print("Couldn't run locmap successfully") - except: - print("Unexpected error:", sys.exc_info()[0]) - diff --git a/src/cern_customizations/categories/__init__.py b/src/cern_customizations/service/__init__.py similarity index 100% rename from src/cern_customizations/categories/__init__.py rename to src/cern_customizations/service/__init__.py diff --git a/src/cern_customizations/service/__main__.py b/src/cern_customizations/service/__main__.py new file mode 100644 index 0000000..b3db71c --- /dev/null +++ b/src/cern_customizations/service/__main__.py @@ -0,0 +1,6 @@ +from pyanaconda.modules.common import init +init() # must be called before importing the service code + +from cern_customizations.service.cern import CernCustomizations +service = CernCustomizations() +service.run() diff --git a/src/cern_customizations/service/cern.py b/src/cern_customizations/service/cern.py new file mode 100644 index 0000000..d79a6ec --- /dev/null +++ b/src/cern_customizations/service/cern.py @@ -0,0 +1,111 @@ +from pyanaconda.core.configuration.anaconda import conf +from pyanaconda.core.dbus import DBus +from pyanaconda.core.signal import Signal +from pyanaconda.modules.common.base import KickstartService +from pyanaconda.modules.common.containers import TaskContainer + +from cern_customizations.constants import CERN +from cern_customizations.service.cern_interface import CernCustomizationsInterface +from cern_customizations.service.kickstart import CernCustomizationsKickstartSpecification +from cern_customizations.service.installation import CernCustomizationsInstallationTask + + +import logging + +log = logging.getLogger(__name__) + + +class CernCustomizations(KickstartService): + + def __init__(self): + super().__init__() + + self._configured = False + self._afs = True + self._cvmfs = False + self._run = False + self._autoupdate = True + self._gnomeinit = True + + self.configured_changed = Signal() + self.afs_changed = Signal() + self.cvmfs_changed = Signal() + self.run_changed = Signal() + self.autoupdate_changed = Signal() + self.gnomeinit_changed = Signal() + + def publish(self): + """Publish the module.""" + TaskContainer.set_namespace(CERN.namespace) + DBus.publish_object(CERN.object_path, CernCustomizationsInterface(self)) + DBus.register_service(CERN.service_name) + + @property + def kickstart_specification(self): + """Return the kickstart specification.""" + return CernCustomizationsKickstartSpecification + + def process_kickstart(self, data): + """Process the kickstart data.""" + log.debug("Processing kickstart data...") + + def setup_kickstart(self, data): + """Set the given kickstart data.""" + log.debug("Generating kickstart data...") + + # to change + @property + def Configured(self): + return self._configured + + def set_configured(self, configured): + self._configured = configured + self.configured_changed.emit() + + @property + def Afs(self): + return self._afs + + def set_afs(self, afs): + self._afs = afs + self.afs_changed.emit() + + @property + def Cvmfs(self): + return self._cfmfs + + def set_cvmfs(self, cvmfs): + self._cvmfs = cvmfs + self.cvmfs_changed.emit() + + @property + def Run(self): + return self._run + + def set_run(self, run): + self._run = run + self.run_changed.emit() + + @property + def AutoUpdate(self): + return self._autoupdate + + def set_autoupdate(self, autoupdate): + self._autoupdate = autoupdate + self.autoupdate_changed.emit() + + property + def GnomeInit(self): + return self._gnomeinit + + def set_gnomeinit(self, gnomeinit): + self._gnomeinit = gnomeinit + self.gnomeinit_changed.emit() + + def install_with_tasks(self): + """Return installation tasks. + :return: a list of tasks + """ + return [ + CernCustomizationsInstallationTask(run = self._run, afs = self._afs, autoupdate = self._autoupdate, cvmfs = self._cvmfs, gnomeinit = self._gnomeinit) + ] diff --git a/src/cern_customizations/service/cern_interface.py b/src/cern_customizations/service/cern_interface.py new file mode 100644 index 0000000..8832d40 --- /dev/null +++ b/src/cern_customizations/service/cern_interface.py @@ -0,0 +1,67 @@ +from dasbus.server.interface import dbus_interface +from dasbus.server.property import emits_properties_changed +from dasbus.typing import * # pylint: disable=wildcard-import,unused-wildcard-import + +from pyanaconda.modules.common.base import KickstartModuleInterface + +from cern_customizations.constants import CERN + +import logging + +log = logging.getLogger(__name__) + + +@dbus_interface(CERN.interface_name) +class CernCustomizationsInterface(KickstartModuleInterface): + + def connect_signals(self): + super().connect_signals() + + @property + def Configured(self) -> Bool: + return self.implementation.Configured + + @emits_properties_changed + def SetConfigured(self, configured: Bool): + self.implementation.set_configured(configured) + + @property + def Afs(self) -> Bool: + return self.implementation.Afs + + @emits_properties_changed + def SetAfs(self, afs: Bool): + self.implementation.set_afs(afs) + + @property + def Cvmfs(self) -> Bool: + return self.implementation.Cvmfs + + @emits_properties_changed + def SetCvmfs(self, cvmfs: Bool): + self.implementation.set_cvmfs(cvmfs) + + @property + def Run(self) -> Bool: + return self.implementation.Run + + @emits_properties_changed + def SetRun(self, run: Bool): + self.implementation.set_run(run) + + @property + def AutoUpdate(self) -> Bool: + return self.implementation.AutoUpdate + + @emits_properties_changed + def SetAutoUpdate(self, autoupdate: Bool): + self.implementation.set_autoupdate(autoupdate) + + @property + def GnomeInit(self) -> Bool: + return self.implementation.GnomeInit + + @emits_properties_changed + def SetGnomeInit(self, gnomeinit: Bool): + self.implementation.set_gnomeinit(gnomeinit) + diff --git a/src/cern_customizations/service/installation.py b/src/cern_customizations/service/installation.py new file mode 100644 index 0000000..b30fa9b --- /dev/null +++ b/src/cern_customizations/service/installation.py @@ -0,0 +1,165 @@ +import logging +import os +import sys +import subprocess + +from pyanaconda.core import util +from pyanaconda.modules.common.task import Task + +from cern_customizations.constants import SYSCONFIG_FILE_PATH, \ + AFS_IS_DEFAULT, EOS_IS_DEFAULT, CVMFS_IS_DEFAULT, \ + AUTOUPDATE_IS_DEFAULT, RUN_IS_DEFAULT, LOCMAP_BIN, \ + SYSTEMCTL_BIN, SYSCONFIG_YUM_PATH, \ + LOCMAP_CORE_MODULES, AUTOUPDATEBOOT_IS_DEFAULT, \ + GNOME_INIT_DEFAULT, GDMCONFIG_FILE_PATH, \ + LOCMAP_SYSTEMD_UNIT, XDGCONFIG_FILE_PATH + + +# C8 +if (sys.version_info > (3, 0)): + try: + from pyanaconda.core.configuration.anaconda import conf + system_root = conf.target.system_root + # older versions + except: + from pyanaconda.core.util import getSysroot + system_root = getSysroot() +# CC7 +else: + from pyanaconda.iutil import getSysroot + system_root = getSysroot() + +log = logging.getLogger(__name__) + +__all__ = [ "CernCustomizationsInstallationTask"] + +class CernCustomizationsInstallationTask(Task): + + def __init__(self, run, afs, autoupdate, cvmfs, gnomeinit): + """Create a task.""" + super().__init__() + self._run = run + self._afs = afs + self._autoupdate = autoupdate + self._cvmfs = cvmfs + self._gnomeinit = gnomeinit + + @property + def name(self): + return "Run CERN customizations" + + def run(self): + """Run the task.""" + + yumupdate_file = os.path.normpath(system_root + SYSCONFIG_YUM_PATH) + # Data is persisted in /root/initial-setup-ks.cfg + sysconfig_file = os.path.normpath(system_root + SYSCONFIG_FILE_PATH) + with open(sysconfig_file, "w") as fobj: + fobj.write("LOCMAP_FIRSTBOOT_START=%s\n" % self._run) + # For compatibility but can be dropped in >= 7.5 + fobj.write("LOCMAP_FIRSTBOOT_STARTAFS=%s\n" % self._afs) + #fobj.write("LOCMAP_FIRSTBOOT_EOSCLIENT=%s\n" % self.eos) + fobj.write("LOCMAP_FIRSTBOOT_CVMFS=%s\n" % self._cvmfs) + fobj.write("LOCMAP_FIRSTBOOT_UPDATE=%s\n" % self._autoupdate) + fobj.close() + + # Allow to disable gnome-initial-setup + gdmconfig_file = os.path.normpath(system_root + GDMCONFIG_FILE_PATH) + xdgconfig_file = os.path.normpath(system_root + XDGCONFIG_FILE_PATH) + if True: + #FIX FIX FIX FIX if not self._gnomeinit: + # C8 + if (sys.version_info > (3, 0)): + from configparser import SafeConfigParser, NoOptionError, NoSectionError + # CC7 + else: + from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError + config = SafeConfigParser() + config.optionxform = str + config.read(gdmconfig_file) + try: + config.set('daemon', 'InitialSetupEnable', 'False') + with open(gdmconfig_file, "w") as fobj: + config.write(fobj) + fobj.close() + except (NoOptionError,NoSectionError): + print("Unexpected error:", sys.exc_info()[0]) + + configxdg = SafeConfigParser() + configxdg.optionxform = str + configxdg.read(xdgconfig_file) + try: + configxdg.set('Desktop Entry', 'X-GNOME-Autostart-enabled', 'false') + with open(xdgconfig_file, "w") as fobj: + configxdg.write(fobj) + fobj.close() + except (NoOptionError,NoSectionError): + print("Unexpected error:", sys.exc_info()[0]) + + # we will need this for later + release=open('/etc/redhat-release','r').read().split(' ')[3].split('.')[0].rstrip() + + # Configure yum-autoupdate + if self._autoupdate: + if release >= '8': + subprocess.call(["dnf", "-y", "install", "yum-autoupdate"]) + else: + yumautoupdate_installed=os.path.isfile('/usr/sbin/yum-autoupdate') + if yumautoupdate_installed == False: + subprocess.call(["yum", "-y", "install", "yum-autoupdate"]) + subprocess.call([SYSTEMCTL_BIN, "enable", "yum-autoupdate"]) + try: + with open(yumupdate_file, "a") as fobj: + fobj.write("\n# initial-setup [cern-anaconda-cern] values\n") + fobj.write("YUMUPDATE=%d\n" % int(self._autoupdate)) + fobj.close() + except IOError: + print("I/O Error couldn't write %s" % (yumupdate_file)) + except: + print("Unexpected error:", sys.exc_info()[0]) + else: + # we use call rather than check_call as we don't care if this fails + # due to yumautoupdate not being installed + if release >= '8': + subprocess.call([SYSTEMCTL_BIN, "disable", "dnf-automatic-install.timer"]) + else: + subprocess.call([SYSTEMCTL_BIN, "disable", "yum-autoupdate"]) + + # locmap may not be installed - force install if it's not + locmap_installed=os.path.isfile('/usr/bin/locmap') + if locmap_installed == False: + # locmap does not reside in CERN repo on 8, install locmap-release + if release >= '8': + subprocess.call(["dnf", "-y", "install", "locmap-release"]) + # 'yum' will work for both 7 and 8 + subprocess.call(["yum", "-y", "install", "locmap"]) + + # the above call to install locmap could have failed if the host is + # outside of CERN... + locmap_installed=os.path.isfile('/usr/bin/locmap') + if locmap_installed == True: + # Enable locmap core modules by default + for module in LOCMAP_CORE_MODULES: + subprocess.call([LOCMAP_BIN, "--enable", module]) + + # Configure and run locmap only if not disabled + if self.run: + subprocess.call([SYSTEMCTL_BIN, "enable", LOCMAP_SYSTEMD_UNIT]) + try: + if self._afs: + subprocess.check_call([LOCMAP_BIN, "--enable", "afs"]) + else: + subprocess.check_call([LOCMAP_BIN, "--disable", "afs"]) + except subprocess.CalledProcessError: + print("Couldn't run locmap successfully") + except: + print("Unexpected error:", sys.exc_info()[0]) + try: + if self._cvmfs: + subprocess.check_call([LOCMAP_BIN, "--enable", "cvmfs"]) + else: + subprocess.check_call([LOCMAP_BIN, "--disable", "cvmfs"]) + except subprocess.CalledProcessError: + print("Couldn't run locmap successfully") + except: + print("Unexpected error:", sys.exc_info()[0]) diff --git a/src/cern_customizations/service/kickstart.py b/src/cern_customizations/service/kickstart.py new file mode 100644 index 0000000..d7f239e --- /dev/null +++ b/src/cern_customizations/service/kickstart.py @@ -0,0 +1,194 @@ + +"""Module with the CernData class.""" + +import os.path +import subprocess +import sys + +import logging + +from pyanaconda.core.kickstart import VERSION, KickstartSpecification +from pyanaconda.core.kickstart.addon import AddonData + +from inspect import signature, Parameter +# C8 +if (sys.version_info > (3, 0)): + try: + from pyanaconda.core.configuration.anaconda import conf + system_root = conf.target.system_root + # older versions + except: + from pyanaconda.core.util import getSysroot + system_root = getSysroot() +# CC7 +else: + from pyanaconda.iutil import getSysroot + system_root = getSysroot() +from cern_customizations.constants import SYSCONFIG_FILE_PATH, \ + AFS_IS_DEFAULT, EOS_IS_DEFAULT, CVMFS_IS_DEFAULT, \ + AUTOUPDATE_IS_DEFAULT, RUN_IS_DEFAULT, LOCMAP_BIN, \ + SYSTEMCTL_BIN, SYSCONFIG_YUM_PATH, \ + LOCMAP_CORE_MODULES, AUTOUPDATEBOOT_IS_DEFAULT, \ + GNOME_INIT_DEFAULT, GDMCONFIG_FILE_PATH, \ + LOCMAP_SYSTEMD_UNIT, XDGCONFIG_FILE_PATH + +from pykickstart.options import KSOptionParser +from pykickstart.errors import KickstartParseError, formatErrorMsg + +from pykickstart.version import versionToLongString, RHEL8 + +log = logging.getLogger(__name__) + +class CernCustomizationsData(AddonData): + """ + Class parsing and storing data for the CERN addon. + + :see: pyanaconda.addons.AddonData + """ + def __init__(self): + """ + Initialisation + + :param name: name of the addon + :type name: str + """ + super().__init__() + self.run = RUN_IS_DEFAULT + self.afs = AFS_IS_DEFAULT + self.autoupdate = AUTOUPDATE_IS_DEFAULT + self.autoupdateboot = AUTOUPDATEBOOT_IS_DEFAULT + #self.eos = EOS_IS_DEFAULT + self.cvmfs = CVMFS_IS_DEFAULT + self.gnomeinit = GNOME_INIT_DEFAULT + self.configured = False + + def __str__(self): + """ + What should end up in the resulting kickstart file. + """ + addon_str = "%%addon cern_customizations " + if self.run: + addon_str += " --run-locmap" + if self.afs: + addon_str += " --afs-client" + if self.cvmfs: + addon_str += " --cvmfs-client" + #if self.eos: + # addon_str += " --eos-client" + if self.autoupdate: + addon_str += " --auto-update" + if self.autoupdateboot: + addon_str += " --auto-update-boot" + if self.gnomeinit: + addon_str += " --gnome-initial-setup" + addon_str += "\n%end\n" + return addon_str + + def handle_header(self, lineno, args): + """ + The handle_header method is called to parse additional arguments in the + %addon section line. + args is a list of all the arguments following the addon ID. For + example, for the line: + %addon org_fedora_hello_world --reverse --arg2="example" + handle_header will be called with args=['--reverse', '--arg2="example"'] + + :param lineno: the current line number in the kickstart file + :type lineno: int + :param args: the list of arguments from the %addon line + :type args: list + """ + + opc = KSOptionParser(version=RHEL8, description='CERN addon',prog='CERN') + opc.add_argument("--afs-client", action="store_true", default=True, + dest="afs", help="Install and configure Openafs client", + version=RHEL8) + + opc.add_argument("--auto-update", action="store_true", default=True, + dest="autoupdate", help="Enable automatic system updates", + version=RHEL8) + + opc.add_argument("--auto-update-boot", action="store_true", default=False, + dest="autoupdateboot", help="Enable automatic system updates at boot", + version=RHEL8) + + opc.add_argument("--cvmfs-client", action="store_true", default=False, + dest="cvmfs", help="Install and configure Fuse CVMFS client", + version=RHEL8) + + opc.add_argument("--gnome-initial-setup", action="store_true", default=False, + dest="gnomeinit", help="Run Gnome initial setup after first login", + version=RHEL8) + + opc.add_argument("--run-locmap", action="store_true", default=True, + dest="run", help="Run locmap at firstboot", + version=RHEL8) + opts = opc.parse_args(args=args, lineno=lineno) + + self.afs = opts.afs + self.run = opts.run + self.cvmfs = opts.cvmfs + self.autoupdate = opts.autoupdate + self.autoupdateboot = opts.autoupdateboot + self.gnomeinit = opts.gnomeinit + self.configured = True + + def handle_line(self, line): + """ + The handle_line method that is called with every line from this addon's + %addon section of the kickstart file. + + :param line: a single line from the %addon section + :type line: str + """ + pass + + def finalize(self): + """ + The finalize method that is called when the end of the %addon section + (i.e. the %end line) is processed. An addon should check if it has all + required data. If not, it may handle the case quietly or it may raise + the KickstartValueError exception. + """ + pass + + def setup(AddonDataSetupArgs): + """ + The setup method that should make changes to the runtime environment + according to the data stored in this object. + + :param storage: object storing storage-related information + (disks, partitioning, bootloader, etc.) + :type storage: blivet.Blivet instance + :param ksdata: data parsed from the kickstart file and set in the + installation process + :type ksdata: pykickstart.base.BaseHandler instance + :param instclass: distribution-specific information + :type instclass: pyanaconda.installclass.BaseInstallClass + :param payload: object managing packages and environment groups + for the installation + :type payload: any class inherited from the pyanaconda.packaging.Payload + class + """ + pass + + @property + def mandatory(self): + """ + The mandatory property that tells whether the spoke is mandatory to be + completed to continue in the installation process. + + :rtype: bool + + """ + + # this is a mandatory spoke, user must check it + return True + +class CernCustomizationsKickstartSpecification(KickstartSpecification): + + version = VERSION + + addons = { + "cern_customizations": CernCustomizationsData + } diff --git a/src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.conf b/src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.conf new file mode 100644 index 0000000..9976b21 --- /dev/null +++ b/src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.conf @@ -0,0 +1,13 @@ +<!DOCTYPE busconfig PUBLIC + "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <policy user="root"> + <allow own="org.fedoraproject.Anaconda.Addons.CernCustomizations"/> + <allow send_destination="org.fedoraproject.Anaconda.Addons.CernCustomizations"/> + </policy> + <policy context="default"> + <deny own="org.fedoraproject.Anaconda.Addons.CernCustomizations"/> + <allow send_destination="org.fedoraproject.Anaconda.Addons.CernCustomizations"/> + </policy> +</busconfig> diff --git a/src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.service b/src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.service new file mode 100644 index 0000000..bd0b021 --- /dev/null +++ b/src/data/org.fedoraproject.Anaconda.Addons.CernCustomizations.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.fedoraproject.Anaconda.Addons.CernCustomizations +Exec=/usr/libexec/anaconda/start-module cern_customizations.service +User=root -- GitLab