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