"""
Cobbler app-wide settings
"""
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: Copyright 2006-2008, Red Hat, Inc and Others
# SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
# SPDX-FileCopyrightText: 2021 Dominik Gedon <dgedon@suse.de>
# SPDX-FileCopyrightText: 2021 Enno Gotthold <egotthold@suse.de>
# SPDX-FileCopyrightText: Copyright SUSE LLC

import datetime
import logging
import os.path
import pathlib
import shutil
import traceback
from pathlib import Path
from typing import Any, Dict, Hashable
import yaml
from schema import SchemaError, SchemaMissingKeyError, SchemaWrongKeyError

from cobbler.utils import input_converters
from cobbler.settings import migrations


class Settings:
    """
    This class contains all app-wide settings of Cobbler. It should only exist once in a Cobbler instance.
    """

    @staticmethod
    def collection_type() -> str:
        """
        This is a hardcoded string which represents the collection type.

        :return: "setting"
        """
        return "setting"

    @staticmethod
    def collection_types() -> str:
        """
        return the collection plural name
        """
        return "settings"

    def __init__(self):
        """
        Constructor.
        """
        self.auto_migrate_settings = False
        self.allow_duplicate_hostnames = False
        self.allow_duplicate_ips = False
        self.allow_duplicate_macs = False
        self.allow_dynamic_settings = False
        self.always_write_dhcp_entries = False
        self.anamon_enabled = False
        self.auth_token_expiration = 3600
        self.authn_pam_service = "login"
        self.autoinstall_snippets_dir = "/var/lib/cobbler/snippets"
        self.autoinstall_templates_dir = "/var/lib/cobbler/templates"
        self.bind_chroot_path = ""
        self.bind_zonefile_path = "/var/lib/named"
        self.bind_master = "127.0.0.1"
        self.boot_loader_conf_template_dir = "/etc/cobbler/boot_loader_conf"
        self.bootloaders_dir = "/var/lib/cobbler/loaders"
        self.bootloaders_shim_folder = "/usr/share/efi/*/"
        self.bootloaders_shim_file = r"shim\.efi$"
        self.bootloaders_ipxe_folder = "/usr/share/ipxe/"
        self.bootloaders_formats = {
            "aarch64": {"binary_name": "grubaa64.efi"},
            "arm": {"binary_name": "bootarm.efi"},
            "arm64-efi": {
                "binary_name": "grubaa64.efi",
                "extra_modules": ["efinet"],
            },
            "i386-efi": {"binary_name": "bootia32.efi"},
            "i386-pc-pxe": {
                "binary_name": "grub.0",
                "mod_dir": "i386-pc",
                "extra_modules": ["chain", "pxe", "biosdisk"],
            },
            "i686": {"binary_name": "bootia32.efi"},
            "IA64": {"binary_name": "bootia64.efi"},
            "powerpc-ieee1275": {
                "binary_name": "grub.ppc64le",
                "extra_modules": ["net", "ofnet"],
            },
            "x86_64-efi": {
                "binary_name": "grubx86.efi",
                "extra_modules": ["chain", "efinet"],
            },
        }
        self.bootloaders_modules = [
            "btrfs",
            "ext2",
            "xfs",
            "jfs",
            "reiserfs",
            "all_video",
            "boot",
            "cat",
            "configfile",
            "echo",
            "fat",
            "font",
            "gfxmenu",
            "gfxterm",
            "gzio",
            "halt",
            "iso9660",
            "jpeg",
            "linux",
            "loadenv",
            "minicmd",
            "normal",
            "part_apple",
            "part_gpt",
            "part_msdos",
            "password_pbkdf2",
            "png",
            "reboot",
            "search",
            "search_fs_file",
            "search_fs_uuid",
            "search_label",
            "sleep",
            "test",
            "true",
            "video",
            "mdraid09",
            "mdraid1x",
            "lvm",
            "serial",
            "regexp",
            "tr",
            "tftp",
            "http",
            "luks",
            "gcry_rijndael",
            "gcry_sha1",
            "gcry_sha256",
        ]
        self.grubconfig_dir = "/var/lib/cobbler/grub_config"
        self.build_reporting_enabled = False
        self.build_reporting_email = []
        self.build_reporting_ignorelist = []
        self.build_reporting_sender = ""
        self.build_reporting_smtp_server = "localhost"
        self.build_reporting_subject = ""
        self.buildisodir = "/var/cache/cobbler/buildiso"
        self.cheetah_import_whitelist = ["re", "random", "time"]
        self.client_use_https = False
        self.client_use_localhost = False
        self.cobbler_master = ""
        self.convert_server_to_ip = False
        self.createrepo_flags = "-c cache -s sha"
        self.autoinstall = "default.ks"
        self.default_name_servers = []
        self.default_name_servers_search = []
        self.default_ownership = ["admin"]
        self.default_password_crypted = r"\$1\$mF86/UHC\$WvcIcX2t6crBz2onWxyac."
        self.default_template_type = "cheetah"
        self.default_virt_bridge = "xenbr0"
        self.default_virt_disk_driver = "raw"
        self.default_virt_file_size = 5.0
        self.default_virt_ram = 512
        self.default_virt_type = "xenpv"
        self.enable_ipxe = False
        self.enable_menu = True
        self.grub2_mod_dir = "/usr/share/grub2/"
        self.http_port = 80
        self.iso_template_dir = "/etc/cobbler/iso"
        self.jinja2_includedir = "/var/lib/cobbler/jinja2"
        self.kernel_options = {}
        self.ldap_anonymous_bind = True
        self.ldap_base_dn = "DC=devel,DC=redhat,DC=com"
        self.ldap_port = 389
        self.ldap_search_bind_dn = ""
        self.ldap_search_passwd = ""
        self.ldap_search_prefix = "uid="
        self.ldap_server = "grimlock.devel.redhat.com"
        self.ldap_tls = True
        self.ldap_tls_cacertdir = ""
        self.ldap_tls_cacertfile = ""
        self.ldap_tls_certfile = ""
        self.ldap_tls_keyfile = ""
        self.ldap_tls_reqcert = "hard"
        self.ldap_tls_cipher_suite = ""
        self.bind_manage_ipmi = False
        # TODO: Remove following line
        self.manage_dhcp = False
        self.manage_dhcp_v6 = False
        self.manage_dhcp_v4 = False
        self.manage_dns = False
        self.manage_forward_zones = []
        self.manage_reverse_zones = []
        self.manage_genders = False
        self.manage_rsync = False
        self.manage_tftpd = True
        self.mgmt_classes = []
        self.mgmt_parameters = {"from_cobbler": 1}
        self.next_server_v4 = "127.0.0.1"
        self.next_server_v6 = "::1"
        self.nsupdate_enabled = False
        self.nsupdate_log = "/var/log/cobbler/nsupdate.log"
        self.nsupdate_tsig_algorithm = "hmac-sha512"
        self.nsupdate_tsig_key = []
        self.power_management_default_type = "ipmilanplus"
        self.proxies = []
        self.proxy_url_ext = ""
        self.proxy_url_int = ""
        self.puppet_auto_setup = False
        self.puppet_parameterized_classes = True
        self.puppet_server = "puppet"
        self.puppet_version = 2
        self.puppetca_path = "/usr/bin/puppet"
        self.pxe_just_once = True
        self.nopxe_with_triggers = True
        self.redhat_management_permissive = False
        self.redhat_management_server = "xmlrpc.rhn.redhat.com"
        self.redhat_management_key = ""
        self.register_new_installs = False
        self.remove_old_puppet_certs_automatically = False
        self.replicate_repo_rsync_options = "-avzH"
        self.replicate_rsync_options = "-avzH"
        self.reposync_flags = "-l -m -d"
        self.reposync_rsync_flags = ""
        self.restart_dhcp = True
        self.restart_dns = True
        self.run_install_triggers = True
        self.scm_track_enabled = False
        self.scm_track_mode = "git"
        self.scm_track_author = "cobbler <cobbler@localhost>"
        self.scm_push_script = "/bin/true"
        self.serializer_pretty_json = False
        self.server = "127.0.0.1"
        self.sign_puppet_certs_automatically = False
        self.signature_path = "/var/lib/cobbler/distro_signatures.json"
        self.signature_url = "https://cobbler.github.io/signatures/3.0.x/latest.json"
        self.syslinux_dir = "/usr/share/syslinux"
        self.syslinux_memdisk_folder = "/usr/share/syslinux"
        self.syslinux_pxelinux_folder = "/usr/share/syslinux"
        self.tftpboot_location = "/var/lib/tftpboot"
        self.virt_auto_boot = True
        self.webdir = "/var/www/cobbler"
        self.webdir_whitelist = [
            ".link_cache",
            "misc",
            "distro_mirror",
            "images",
            "links",
            "localmirror",
            "pub",
            "rendered",
            "repo_mirror",
            "repo_profile",
            "repo_system",
            "svc",
            "web",
            "webui",
        ]
        self.xmlrpc_port = 25151
        self.yum_distro_priority = 1
        self.yum_post_install_mirror = True
        self.yumdownloader_flags = "--resolve"
        self.windows_enabled = False
        self.windows_template_dir = "/etc/cobbler/windows"
        self.samba_distro_share = "DISTRO"

    def to_string(self) -> str:
        """
        Returns the kernel options as a string.

        :return: The multiline string with the kernel options.
        """
        buf = "defaults\n"
        buf += "kernel options  : %s\n" % self.__dict__["kernel_options"]
        return buf

    def to_dict(self, resolved: bool = False) -> dict:
        """
        Return an easily serializable representation of the config.

        .. deprecated:: 3.2.1
           Use ``obj.__dict__`` directly please. Will be removed with 3.3.0

        :param resolved: Present for the compatibility with the Cobbler collections.
        :return: The dict with all user settings combined with settings which are left to the default.
        """
        # TODO: Deprecate and remove. Tailcall is not needed.
        return self.__dict__

    def from_dict(self, new_values: dict):
        """
        Modify this object to load values in dictionary. If the handed dict would lead to an invalid object it is
        silently discarded.

        .. warning:: If the dict from the args has not all settings included Cobbler may behave unexpectedly.

        :param new_values: The dictionary with settings to replace.
        :return: Returns the settings instance this method was called from.
        """
        if new_values is None:
            logging.warning("Not loading empty settings dictionary!")
            return

        old_settings = self.__dict__
        self.__dict__.update(new_values)

        if not self.is_valid():
            self.__dict__ = old_settings
            raise ValueError(
                "New settings would not be valid. Please fix the dict you pass."
            )

        return self

    def is_valid(self) -> bool:
        """
        Silently drops all errors and returns ``True`` when everything is valid.

        :return: If this settings object is valid this returns true. Otherwise false.
        """
        try:
            validate_settings(self.__dict__)
        except SchemaError:
            return False
        return True

    def __getattr__(self, name):
        """
        This returns the current value of the setting named in the args.

        :param name: The setting to return the value of.
        :return: The value of the setting "name".
        """
        try:
            if name == "kernel_options":
                # backwards compatibility -- convert possible string value to dict
                result = input_converters.input_string_or_dict(
                    self.__dict__[name], allow_multiples=False
                )
                self.__dict__[name] = result
                return result
            # TODO: This needs to be explicitly tested
            elif name == "manage_dhcp":
                return self.manage_dhcp_v4
            return self.__dict__[name]
        except Exception as error:
            if name in self.__dict__:
                return self.__dict__[name]
            else:
                raise AttributeError(
                    f"no settings attribute named '{name}' found"
                ) from error

    def save(self, filepath="/etc/cobbler/settings.yaml"):
        """
        Saves the settings to the disk.
        """
        update_settings_file(self.to_dict(), filepath)


def validate_settings(settings_content: dict) -> dict:
    """
    This function performs logical validation of our loaded YAML files.
    This function will:
    - Perform type validation on all values of all keys.
    - Provide defaults for optional settings.
    :param settings_content: The dictionary content from the YAML file.
    :raises SchemaError: In case the data given is invalid.
    :return: The Settings of Cobbler which can be safely used inside this instance.
    """
    return migrations.normalize(settings_content)


def read_yaml_file(filepath="/etc/cobbler/settings.yaml") -> Dict[Hashable, Any]:
    """
    Reads settings files from ``filepath`` and saves the content in a dictionary.

    :param filepath: Settings file path, defaults to "/ect/cobbler/settings.yaml"
    :raises FileNotFoundError: In case file does not exist or is a directory.
    :raises yaml.YAMLError: In case the file is not a valid YAML file.
    :return: The aggregated dict of all settings.
    """
    if not os.path.isfile(filepath):
        raise FileNotFoundError(
            'Given path "%s" does not exist or is a directory.' % filepath
        )
    try:
        with open(filepath) as main_settingsfile:
            filecontent = yaml.safe_load(main_settingsfile.read())
    except yaml.YAMLError as error:
        traceback.print_exc()
        raise yaml.YAMLError('"%s" is not a valid YAML file' % filepath) from error
    return filecontent


def read_settings_file(filepath="/etc/cobbler/settings.yaml") -> Dict[Hashable, Any]:
    """
    Utilizes ``read_yaml_file()``. If the read settings file is invalid in the context of Cobbler we will return an
    empty dictionary.

    :param filepath: The path to the settings file.
    :raises SchemaMissingKeyError: In case keys are minssing.
    :raises SchemaWrongKeyError: In case keys are not listed in the schema.
    :raises SchemaError: In case the schema is wrong.
    :return: A dictionary with the settings. As a word of caution: This may not represent a correct settings object, it
             will only contain a correct YAML representation.
    """
    filecontent = read_yaml_file(filepath)

    # FIXME: Do not call validate_settings() because of chicken - egg problem
    try:
        validate_settings(filecontent)
    except SchemaMissingKeyError:
        logging.exception("Settings file was not returned due to missing keys.")
        logging.debug('The settings to read were: "%s"', filecontent)
        return {}
    except SchemaWrongKeyError:
        logging.exception("Settings file was returned due to an error in the schema.")
        logging.debug('The settings to read were: "%s"', filecontent)
        return {}
    except SchemaError:
        logging.exception("Settings file was returned due to an error in the schema.")
        logging.debug('The settings to read were: "%s"', filecontent)
        return {}
    return filecontent


def update_settings_file(data: dict, filepath="/etc/cobbler/settings.yaml") -> bool:
    """
    Write data handed to this function into the settings file of Cobbler. This function overwrites the existing content.
    It will only write valid settings. If you are trying to save invalid data this will raise a SchemaException
    described in :py:meth:`cobbler.settings.validate`.

    :param data: The data to put into the settings file.
    :param filepath: This sets the path of the settingsfile to write.
    :return: True if the action succeeded. Otherwise return False.
    """
    # Backup old settings file
    path = pathlib.Path(filepath)
    if path.exists():
        timestamp = str(datetime.datetime.now().strftime("%Y%m%d_%H-%M-%S"))
        shutil.copy(path, path.parent.joinpath(f"{path.stem}_{timestamp}{path.suffix}"))

    try:
        validated_data = validate_settings(data)
        with open(filepath, "w") as settings_file:
            yaml_dump = yaml.safe_dump(validated_data)
            header = "# Cobbler settings file\n"
            header += "# Docs for this file can be found at: https://cobbler.readthedocs.io/en/latest/cobbler-conf.html"
            header += "\n\n"
            yaml_dump = header + yaml_dump
            settings_file.write(yaml_dump)
        return True
    except SchemaMissingKeyError:
        logging.exception(
            "Settings file was not written to the disc due to missing keys."
        )
        logging.debug('The settings to write were: "%s"', data)
        return False
    except SchemaError:
        logging.exception(
            "Settings file was not written to the disc due to an error in the schema."
        )
        logging.debug('The settings to write were: "%s"', data)
        return False


def migrate(yaml_dict: dict, settings_path: Path) -> dict:
    """
    Migrates the current settings

    :param yaml_dict: The settings dict
    :param settings_path: The settings path
    :return: The migrated settings
    """
    return migrations.migrate(yaml_dict, settings_path)

cobbler/settings/__init__.py
"""
Cobbler app-wide settings
"""
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: Copyright 2006-2008, Red Hat, Inc and Others
# SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
# SPDX-FileCopyrightText: 2021 Dominik Gedon <dgedon@suse.de>
# SPDX-FileCopyrightText: 2021 Enno Gotthold <egotthold@suse.de>
# SPDX-FileCopyrightText: Copyright SUSE LLC

import datetime
import logging
import os.path
import pathlib
import shutil
import traceback
from pathlib import Path
from typing import Any, Dict, Hashable
import yaml
from schema import SchemaError, SchemaMissingKeyError, SchemaWrongKeyError

from cobbler.utils import input_converters
from cobbler.settings import migrations


class Settings:
    """
    This class contains all app-wide settings of Cobbler. It should only exist once in a Cobbler instance.
    """

    @staticmethod
    def collection_type() -> str:
        """
        This is a hardcoded string which represents the collection type.

        :return: "setting"
        """
        return "setting"

    @staticmethod
    def collection_types() -> str:
        """
        return the collection plural name
        """
        return "settings"

    def __init__(self):
        """
        Constructor.
        """
        self.auto_migrate_settings = False
        self.allow_duplicate_hostnames = False
        self.allow_duplicate_ips = False
        self.allow_duplicate_macs = False
        self.allow_dynamic_settings = False
        self.always_write_dhcp_entries = False
        self.anamon_enabled = False
        self.auth_token_expiration = 3600
        self.authn_pam_service = "login"
        self.autoinstall_snippets_dir = "/var/lib/cobbler/snippets"
        self.autoinstall_templates_dir = "/var/lib/cobbler/templates"
        self.bind_chroot_path = ""
        self.bind_zonefile_path = "/var/lib/named"
        self.bind_master = "127.0.0.1"
        self.boot_loader_conf_template_dir = "/etc/cobbler/boot_loader_conf"
        self.bootloaders_dir = "/var/lib/cobbler/loaders"
        self.bootloaders_shim_folder = "/usr/share/efi/*/"
        self.bootloaders_shim_file = r"shim\.efi$"
        self.bootloaders_ipxe_folder = "/usr/share/ipxe/"
        self.bootloaders_formats = {
            "aarch64": {"binary_name": "grubaa64.efi"},
            "arm": {"binary_name": "bootarm.efi"},
            "arm64-efi": {
                "binary_name": "grubaa64.efi",
                "extra_modules": ["efinet"],
            },
            "i386-efi": {"binary_name": "bootia32.efi"},
            "i386-pc-pxe": {
                "binary_name": "grub.0",
                "mod_dir": "i386-pc",
                "extra_modules": ["chain", "pxe", "biosdisk"],
            },
            "i686": {"binary_name": "bootia32.efi"},
            "IA64": {"binary_name": "bootia64.efi"},
            "powerpc-ieee1275": {
                "binary_name": "grub.ppc64le",
                "extra_modules": ["net", "ofnet"],
            },
            "x86_64-efi": {
                "binary_name": "grubx64.efi",
                "extra_modules": ["chain", "efinet"],
            },
        }
        self.bootloaders_modules = [
            "btrfs",
            "ext2",
            "xfs",
            "jfs",
            "reiserfs",
            "all_video",
            "boot",
            "cat",
            "configfile",
            "echo",
            "fat",
            "font",
            "gfxmenu",
            "gfxterm",
            "gzio",
            "halt",
            "iso9660",
            "jpeg",
            "linux",
            "loadenv",
            "minicmd",
            "normal",
            "part_apple",
            "part_gpt",
            "part_msdos",
            "password_pbkdf2",
            "png",
            "reboot",
            "search",
            "search_fs_file",
            "search_fs_uuid",
            "search_label",
            "sleep",
            "test",
            "true",
            "video",
            "mdraid09",
            "mdraid1x",
            "lvm",
            "serial",
            "regexp",
            "tr",
            "tftp",
            "http",
            "luks",
            "gcry_rijndael",
            "gcry_sha1",
            "gcry_sha256",
        ]
        self.grubconfig_dir = "/var/lib/cobbler/grub_config"
        self.build_reporting_enabled = False
        self.build_reporting_email = []
        self.build_reporting_ignorelist = []
        self.build_reporting_sender = ""
        self.build_reporting_smtp_server = "localhost"
        self.build_reporting_subject = ""
        self.buildisodir = "/var/cache/cobbler/buildiso"
        self.cheetah_import_whitelist = ["re", "random", "time"]
        self.client_use_https = False
        self.client_use_localhost = False
        self.cobbler_master = ""
        self.convert_server_to_ip = False
        self.createrepo_flags = "-c cache -s sha"
        self.autoinstall = "default.ks"
        self.default_name_servers = []
        self.default_name_servers_search = []
        self.default_ownership = ["admin"]
        self.default_password_crypted = r"\$1\$mF86/UHC\$WvcIcX2t6crBz2onWxyac."
        self.default_template_type = "cheetah"
        self.default_virt_bridge = "xenbr0"
        self.default_virt_disk_driver = "raw"
        self.default_virt_file_size = 5.0
        self.default_virt_ram = 512
        self.default_virt_type = "xenpv"
        self.enable_ipxe = False
        self.enable_menu = True
        self.grub2_mod_dir = "/usr/share/grub2/"
        self.http_port = 80
        self.iso_template_dir = "/etc/cobbler/iso"
        self.jinja2_includedir = "/var/lib/cobbler/jinja2"
        self.kernel_options = {}
        self.ldap_anonymous_bind = True
        self.ldap_base_dn = "DC=devel,DC=redhat,DC=com"
        self.ldap_port = 389
        self.ldap_search_bind_dn = ""
        self.ldap_search_passwd = ""
        self.ldap_search_prefix = "uid="
        self.ldap_server = "grimlock.devel.redhat.com"
        self.ldap_tls = True
        self.ldap_tls_cacertdir = ""
        self.ldap_tls_cacertfile = ""
        self.ldap_tls_certfile = ""
        self.ldap_tls_keyfile = ""
        self.ldap_tls_reqcert = "hard"
        self.ldap_tls_cipher_suite = ""
        self.bind_manage_ipmi = False
        # TODO: Remove following line
        self.manage_dhcp = False
        self.manage_dhcp_v6 = False
        self.manage_dhcp_v4 = False
        self.manage_dns = False
        self.manage_forward_zones = []
        self.manage_reverse_zones = []
        self.manage_genders = False
        self.manage_rsync = False
        self.manage_tftpd = True
        self.mgmt_classes = []
        self.mgmt_parameters = {"from_cobbler": 1}
        self.next_server_v4 = "127.0.0.1"
        self.next_server_v6 = "::1"
        self.nsupdate_enabled = False
        self.nsupdate_log = "/var/log/cobbler/nsupdate.log"
        self.nsupdate_tsig_algorithm = "hmac-sha512"
        self.nsupdate_tsig_key = []
        self.power_management_default_type = "ipmilanplus"
        self.proxies = []
        self.proxy_url_ext = ""
        self.proxy_url_int = ""
        self.puppet_auto_setup = False
        self.puppet_parameterized_classes = True
        self.puppet_server = "puppet"
        self.puppet_version = 2
        self.puppetca_path = "/usr/bin/puppet"
        self.pxe_just_once = True
        self.nopxe_with_triggers = True
        self.redhat_management_permissive = False
        self.redhat_management_server = "xmlrpc.rhn.redhat.com"
        self.redhat_management_key = ""
        self.register_new_installs = False
        self.remove_old_puppet_certs_automatically = False
        self.replicate_repo_rsync_options = "-avzH"
        self.replicate_rsync_options = "-avzH"
        self.reposync_flags = "-l -m -d"
        self.reposync_rsync_flags = ""
        self.restart_dhcp = True
        self.restart_dns = True
        self.run_install_triggers = True
        self.scm_track_enabled = False
        self.scm_track_mode = "git"
        self.scm_track_author = "cobbler <cobbler@localhost>"
        self.scm_push_script = "/bin/true"
        self.serializer_pretty_json = False
        self.server = "127.0.0.1"
        self.sign_puppet_certs_automatically = False
        self.signature_path = "/var/lib/cobbler/distro_signatures.json"
        self.signature_url = "https://cobbler.github.io/signatures/3.0.x/latest.json"
        self.syslinux_dir = "/usr/share/syslinux"
        self.syslinux_memdisk_folder = "/usr/share/syslinux"
        self.syslinux_pxelinux_folder = "/usr/share/syslinux"
        self.tftpboot_location = "/var/lib/tftpboot"
        self.virt_auto_boot = True
        self.webdir = "/var/www/cobbler"
        self.webdir_whitelist = [
            ".link_cache",
            "misc",
            "distro_mirror",
            "images",
            "links",
            "localmirror",
            "pub",
            "rendered",
            "repo_mirror",
            "repo_profile",
            "repo_system",
            "svc",
            "web",
            "webui",
        ]
        self.xmlrpc_port = 25151
        self.yum_distro_priority = 1
        self.yum_post_install_mirror = True
        self.yumdownloader_flags = "--resolve"
        self.windows_enabled = False
        self.windows_template_dir = "/etc/cobbler/windows"
        self.samba_distro_share = "DISTRO"

    def to_string(self) -> str:
        """
        Returns the kernel options as a string.

        :return: The multiline string with the kernel options.
        """
        buf = "defaults\n"
        buf += "kernel options  : %s\n" % self.__dict__["kernel_options"]
        return buf

    def to_dict(self, resolved: bool = False) -> dict:
        """
        Return an easily serializable representation of the config.

        .. deprecated:: 3.2.1
           Use ``obj.__dict__`` directly please. Will be removed with 3.3.0

        :param resolved: Present for the compatibility with the Cobbler collections.
        :return: The dict with all user settings combined with settings which are left to the default.
        """
        # TODO: Deprecate and remove. Tailcall is not needed.
        return self.__dict__

    def from_dict(self, new_values: dict):
        """
        Modify this object to load values in dictionary. If the handed dict would lead to an invalid object it is
        silently discarded.

        .. warning:: If the dict from the args has not all settings included Cobbler may behave unexpectedly.

        :param new_values: The dictionary with settings to replace.
        :return: Returns the settings instance this method was called from.
        """
        if new_values is None:
            logging.warning("Not loading empty settings dictionary!")
            return

        old_settings = self.__dict__
        self.__dict__.update(new_values)

        if not self.is_valid():
            self.__dict__ = old_settings
            raise ValueError(
                "New settings would not be valid. Please fix the dict you pass."
            )

        return self

    def is_valid(self) -> bool:
        """
        Silently drops all errors and returns ``True`` when everything is valid.

        :return: If this settings object is valid this returns true. Otherwise false.
        """
        try:
            validate_settings(self.__dict__)
        except SchemaError:
            return False
        return True

    def __getattr__(self, name):
        """
        This returns the current value of the setting named in the args.

        :param name: The setting to return the value of.
        :return: The value of the setting "name".
        """
        try:
            if name == "kernel_options":
                # backwards compatibility -- convert possible string value to dict
                result = input_converters.input_string_or_dict(
                    self.__dict__[name], allow_multiples=False
                )
                self.__dict__[name] = result
                return result
            # TODO: This needs to be explicitly tested
            elif name == "manage_dhcp":
                return self.manage_dhcp_v4
            return self.__dict__[name]
        except Exception as error:
            if name in self.__dict__:
                return self.__dict__[name]
            else:
                raise AttributeError(
                    f"no settings attribute named '{name}' found"
                ) from error

    def save(self, filepath="/etc/cobbler/settings.yaml"):
        """
        Saves the settings to the disk.
        """
        update_settings_file(self.to_dict(), filepath)


def validate_settings(settings_content: dict) -> dict:
    """
    This function performs logical validation of our loaded YAML files.
    This function will:
    - Perform type validation on all values of all keys.
    - Provide defaults for optional settings.
    :param settings_content: The dictionary content from the YAML file.
    :raises SchemaError: In case the data given is invalid.
    :return: The Settings of Cobbler which can be safely used inside this instance.
    """
    return migrations.normalize(settings_content)


def read_yaml_file(filepath="/etc/cobbler/settings.yaml") -> Dict[Hashable, Any]:
    """
    Reads settings files from ``filepath`` and saves the content in a dictionary.

    :param filepath: Settings file path, defaults to "/ect/cobbler/settings.yaml"
    :raises FileNotFoundError: In case file does not exist or is a directory.
    :raises yaml.YAMLError: In case the file is not a valid YAML file.
    :return: The aggregated dict of all settings.
    """
    if not os.path.isfile(filepath):
        raise FileNotFoundError(
            'Given path "%s" does not exist or is a directory.' % filepath
        )
    try:
        with open(filepath) as main_settingsfile:
            filecontent = yaml.safe_load(main_settingsfile.read())
    except yaml.YAMLError as error:
        traceback.print_exc()
        raise yaml.YAMLError('"%s" is not a valid YAML file' % filepath) from error
    return filecontent


def read_settings_file(filepath="/etc/cobbler/settings.yaml") -> Dict[Hashable, Any]:
    """
    Utilizes ``read_yaml_file()``. If the read settings file is invalid in the context of Cobbler we will return an
    empty dictionary.

    :param filepath: The path to the settings file.
    :raises SchemaMissingKeyError: In case keys are minssing.
    :raises SchemaWrongKeyError: In case keys are not listed in the schema.
    :raises SchemaError: In case the schema is wrong.
    :return: A dictionary with the settings. As a word of caution: This may not represent a correct settings object, it
             will only contain a correct YAML representation.
    """
    filecontent = read_yaml_file(filepath)

    # FIXME: Do not call validate_settings() because of chicken - egg problem
    try:
        validate_settings(filecontent)
    except SchemaMissingKeyError:
        logging.exception("Settings file was not returned due to missing keys.")
        logging.debug('The settings to read were: "%s"', filecontent)
        return {}
    except SchemaWrongKeyError:
        logging.exception("Settings file was returned due to an error in the schema.")
        logging.debug('The settings to read were: "%s"', filecontent)
        return {}
    except SchemaError:
        logging.exception("Settings file was returned due to an error in the schema.")
        logging.debug('The settings to read were: "%s"', filecontent)
        return {}
    return filecontent


def update_settings_file(data: dict, filepath="/etc/cobbler/settings.yaml") -> bool:
    """
    Write data handed to this function into the settings file of Cobbler. This function overwrites the existing content.
    It will only write valid settings. If you are trying to save invalid data this will raise a SchemaException
    described in :py:meth:`cobbler.settings.validate`.

    :param data: The data to put into the settings file.
    :param filepath: This sets the path of the settingsfile to write.
    :return: True if the action succeeded. Otherwise return False.
    """
    # Backup old settings file
    path = pathlib.Path(filepath)
    if path.exists():
        timestamp = str(datetime.datetime.now().strftime("%Y%m%d_%H-%M-%S"))
        shutil.copy(path, path.parent.joinpath(f"{path.stem}_{timestamp}{path.suffix}"))

    try:
        validated_data = validate_settings(data)
        with open(filepath, "w") as settings_file:
            yaml_dump = yaml.safe_dump(validated_data)
            header = "# Cobbler settings file\n"
            header += "# Docs for this file can be found at: https://cobbler.readthedocs.io/en/latest/cobbler-conf.html"
            header += "\n\n"
            yaml_dump = header + yaml_dump
            settings_file.write(yaml_dump)
        return True
    except SchemaMissingKeyError:
        logging.exception(
            "Settings file was not written to the disc due to missing keys."
        )
        logging.debug('The settings to write were: "%s"', data)
        return False
    except SchemaError:
        logging.exception(
            "Settings file was not written to the disc due to an error in the schema."
        )
        logging.debug('The settings to write were: "%s"', data)
        return False


def migrate(yaml_dict: dict, settings_path: Path) -> dict:
    """
    Migrates the current settings

    :param yaml_dict: The settings dict
    :param settings_path: The settings path
    :return: The migrated settings
    """
    return migrations.migrate(yaml_dict, settings_path)

cobbler/settings/__init__.py
diff --git a/.github/workflows/system_testing.yml b/.github/workflows/system_testing.yml
index e7c3f2fdf5..450321dde4 100644
--- a/.github/workflows/system_testing.yml
+++ b/.github/workflows/system_testing.yml
@@ -7,7 +7,45 @@ on:
     branches: [ main, release* ]
 
 jobs:
-  run_system_test:
+  basic:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+      - name: Pull Docker Test Container
+        run: docker pull registry.opensuse.org/home/schoolguy/branches/opensuse/templates/images/tumbleweed/containers/opensuse/cobbler-github-ci:latest
+      - name: Run previously built Docker Container
+        run: docker run --cap-add=NET_ADMIN -t -d -v $PWD:/code --name cobbler registry.opensuse.org/home/schoolguy/branches/opensuse/templates/images/tumbleweed/containers/opensuse/cobbler-github-ci:latest
+      - name: Setup Cobbler in the Container
+        shell: 'script -q -e -c "bash {0}"'
+        run: |
+          docker exec -u 0 -it cobbler bash -c "./docker/develop/scripts/setup-supervisor.sh"
+      - name: Run the Tests inside the Docker Container
+        shell: 'script -q -e -c "bash {0}"'
+        run: |
+          docker exec -u 0 -it cobbler bash -c "make SYSTESTS='basic-*' system-test"
+      - name: Stop and remove the container
+        run: docker stop cobbler && docker rm cobbler
+  import:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+      - name: Pull Docker Test Container
+        run: docker pull registry.opensuse.org/home/schoolguy/branches/opensuse/templates/images/tumbleweed/containers/opensuse/cobbler-github-ci:latest
+      - name: Run previously built Docker Container
+        run: docker run --cap-add=NET_ADMIN -t -d -v $PWD:/code --name cobbler registry.opensuse.org/home/schoolguy/branches/opensuse/templates/images/tumbleweed/containers/opensuse/cobbler-github-ci:latest
+      - name: Setup Cobbler in the Container
+        shell: 'script -q -e -c "bash {0}"'
+        run: |
+          docker exec -u 0 -it cobbler bash -c "./docker/develop/scripts/setup-supervisor.sh"
+      - name: Run the Tests inside the Docker Container
+        shell: 'script -q -e -c "bash {0}"'
+        run: |
+          docker exec -u 0 -it cobbler bash -c "make SYSTESTS='import-*' system-test"
+      - name: Stop and remove the container
+        run: docker stop cobbler && docker rm cobbler
+  settings-cli:
     runs-on: ubuntu-20.04
     steps:
       - name: Checkout repository
@@ -23,6 +61,25 @@ jobs:
       - name: Run the Tests inside the Docker Container
         shell: 'script -q -e -c "bash {0}"'
         run: |
-          docker exec -u 0 -it cobbler bash -c "make system-test"
+          docker exec -u 0 -it cobbler bash -c "make SYSTESTS='settings-cli-*' system-test"
+      - name: Stop and remove the container
+        run: docker stop cobbler && docker rm cobbler
+  svc:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+      - name: Pull Docker Test Container
+        run: docker pull registry.opensuse.org/home/schoolguy/branches/opensuse/templates/images/tumbleweed/containers/opensuse/cobbler-github-ci:latest
+      - name: Run previously built Docker Container
+        run: docker run --cap-add=NET_ADMIN -t -d -v $PWD:/code --name cobbler registry.opensuse.org/home/schoolguy/branches/opensuse/templates/images/tumbleweed/containers/opensuse/cobbler-github-ci:latest
+      - name: Setup Cobbler in the Container
+        shell: 'script -q -e -c "bash {0}"'
+        run: |
+          docker exec -u 0 -it cobbler bash -c "./docker/develop/scripts/setup-supervisor.sh"
+      - name: Run the Tests inside the Docker Container
+        shell: 'script -q -e -c "bash {0}"'
+        run: |
+          docker exec -u 0 -it cobbler bash -c "make SYSTESTS='svc-*' system-test"
       - name: Stop and remove the container
         run: docker stop cobbler && docker rm cobbler
diff --git a/cobbler/settings/__init__.py b/cobbler/settings/__init__.py
index c7898f9d0c..19827716ba 100644
--- a/cobbler/settings/__init__.py
+++ b/cobbler/settings/__init__.py
@@ -87,7 +87,7 @@ def __init__(self):
                 "extra_modules": ["net", "ofnet"],
             },
             "x86_64-efi": {
-                "binary_name": "grubx86.efi",
+                "binary_name": "grubx64.efi",
                 "extra_modules": ["chain", "efinet"],
             },
         }
diff --git a/config/cobbler/settings.yaml b/config/cobbler/settings.yaml
index 1262f5f118..970f08d45c 100644
--- a/config/cobbler/settings.yaml
+++ b/config/cobbler/settings.yaml
@@ -74,7 +74,7 @@ bootloaders_formats:
       - net
       - ofnet
   x86_64-efi:
-    binary_name: grubx86.efi
+    binary_name: grubx64.efi
     extra_modules:
       - chain
       - efinet
diff --git a/docker/develop/develop.dockerfile b/docker/develop/develop.dockerfile
index fddb1ab1d2..c825f9cde3 100644
--- a/docker/develop/develop.dockerfile
+++ b/docker/develop/develop.dockerfile
@@ -21,6 +21,8 @@ RUN zypper install --no-recommends -y \
     gzip                       \
     ipmitool                   \
     make                       \
+    curl                       \
+    wget2                      \
     python3                    \
     python3-Sphinx             \
     python3-coverage           \
diff --git a/docker/rpms/Fedora_34/Fedora34.dockerfile b/docker/rpms/Fedora_34/Fedora34.dockerfile
index f038d64565..946bad6201 100644
--- a/docker/rpms/Fedora_34/Fedora34.dockerfile
+++ b/docker/rpms/Fedora_34/Fedora34.dockerfile
@@ -9,6 +9,8 @@ RUN dnf install -y          \
     git                     \
     rsync                   \
     make                    \
+    curl                    \
+    wget2                   \
     openssl                 \
     mod_ssl                 \
     initscripts             \
diff --git a/docker/rpms/Rocky_Linux_8/Rocky_Linux_8.dockerfile b/docker/rpms/Rocky_Linux_8/Rocky_Linux_8.dockerfile
index 7ca75c001e..f1d1600b41 100644
--- a/docker/rpms/Rocky_Linux_8/Rocky_Linux_8.dockerfile
+++ b/docker/rpms/Rocky_Linux_8/Rocky_Linux_8.dockerfile
@@ -20,6 +20,8 @@ RUN touch /var/lib/rpm/* &&   \
     iproute                   \
     git                       \
     rsync                     \
+    curl                      \
+    wget                      \
     make                      \
     openssl                   \
     mod_ssl                   \
diff --git a/docker/rpms/opensuse_leap/openSUSE_Leap153.dockerfile b/docker/rpms/opensuse_leap/openSUSE_Leap153.dockerfile
index 440ab2688c..8c60112a4e 100644
--- a/docker/rpms/opensuse_leap/openSUSE_Leap153.dockerfile
+++ b/docker/rpms/opensuse_leap/openSUSE_Leap153.dockerfile
@@ -13,6 +13,8 @@ RUN zypper install -y          \
     apache2-devel              \
     apache2-mod_wsgi-python3   \
     bash-completion            \
+    curl                       \
+    wget2                      \
     git                        \
     gzip                       \
     make                       \
diff --git a/docker/rpms/opensuse_tumbleweed/openSUSE_TW.dockerfile b/docker/rpms/opensuse_tumbleweed/openSUSE_TW.dockerfile
index 4df0387955..9d3dda722e 100644
--- a/docker/rpms/opensuse_tumbleweed/openSUSE_TW.dockerfile
+++ b/docker/rpms/opensuse_tumbleweed/openSUSE_TW.dockerfile
@@ -15,6 +15,8 @@ RUN zypper install -y          \
     bash-completion            \
     git                        \
     gzip                       \
+    curl                       \
+    wget2                      \
     make                       \
     util-linux                 \
     hardlink                   \
diff --git a/system-tests/prelude b/system-tests/prelude
index a4edf69b88..1267c9d5d7 100644
--- a/system-tests/prelude
+++ b/system-tests/prelude
@@ -1,44 +1,63 @@
+# If you adjust anything in this file please check also with the "scripts/bootstrap" script
+
+#
+# Variables
+#
+
+server=192.168.1.1
+
 #
 # Helpers
 #
 
 _dir() {
-	for dir in ${@}; do
-		test -e ${dir} && echo ${dir} && return
-	done
+    for dir in ${@}; do
+        test -e ${dir} && echo ${dir} && return
+    done
 }
 
 srvctl() {
-	$(command -v supervisorctl systemctl | head -1) ${@}
+    $(command -v supervisorctl systemctl | head -1) ${@}
 }
 
 stop_cobblerd() {
-	srvctl stop cobblerd
+    srvctl stop cobblerd
 }
 
 restart_cobblerd() {
-	srvctl restart cobblerd
-	sleep 1
+    srvctl restart cobblerd
+    sleep 1
 }
 
 _make_system_id() {
-	local id=$(cat ${tmp}/_next-system-id 2>/dev/null)
-	echo $((id + 1)) | tee ${tmp}/_next-system-id
+    local id=$(cat ${tmp}/_next-system-id 2>/dev/null)
+    echo $((id + 1)) | tee ${tmp}/_next-system-id
 }
 
 make_mac_address() {
-	printf "52:54:00"
-	printf "%06x\n" $(_make_system_id) | sed 's/\(..\)/:\1/g'
+    printf "52:54:00"
+    printf "%06x\n" $(_make_system_id) | sed 's/\(..\)/:\1/g'
 }
 
 prepare() {
-	mkdir -p ${tmp}
-	stop_cobblerd
-	rm -f ${path_templates}/${autoinstall_template}
-	rm -f ${path_collections}/*/*.json
-	rm -rf ${path_distro_mirrors}/*
-	restart_cobblerd
-	cobbler sync >/dev/null
+    mkdir -p ${tmp}
+    stop_cobblerd
+    rm -f ${path_templates}/${autoinstall_template}
+    rm -f ${path_collections}/*/*.json
+    rm -rf ${path_distro_mirrors}/*
+    rm /etc/cobbler/settings.yaml*
+    # This is specific for the openSUSE environment
+    cat >/etc/cobbler/settings.yaml <<-EOF
+server: ${server}
+next_server_v4: ${server}
+manage_dhcp: true
+manage_dhcp_v4: true
+manage_dhcp_v6: false
+power_management_default_type: 'ipmilan'
+tftpboot_location: '/srv/tftpboot'
+EOF
+    restart_cobblerd
+    cobbler sync >/dev/null
 }
 
 #
diff --git a/system-tests/scripts/bootstrap b/system-tests/scripts/bootstrap
index 639a92de02..89f66a46bf 100755
--- a/system-tests/scripts/bootstrap
+++ b/system-tests/scripts/bootstrap
@@ -1,5 +1,6 @@
 #!/bin/sh -e
 # Bootstrap the OS for system tests
+# If you adjust anything in this file please check also with the "../prelude" script
 
 server=192.168.1.1
 bridge=pxe
diff --git a/system-tests/scripts/run-tests b/system-tests/scripts/run-tests
index 86e0cbefb1..f7f651775f 100755
--- a/system-tests/scripts/run-tests
+++ b/system-tests/scripts/run-tests
@@ -6,7 +6,7 @@ topdir=$(realpath $(dirname ${0})/..)
 if [ ${#} -ge 1 ]; then
 	patterns=${@}
 else
-	patterns=(basic-* import-* settings-cli-*)
+	patterns=(basic-* import-* settings-cli-* svc-*)
 fi
 
 list_selected_tests() {
diff --git a/system-tests/tests/settings-cli-migrate b/system-tests/tests/settings-cli-migrate
index bc00d0db07..1b52e5d81e 100755
--- a/system-tests/tests/settings-cli-migrate
+++ b/system-tests/tests/settings-cli-migrate
@@ -53,7 +53,7 @@ bootloaders_formats:
       - net
       - ofnet
   x86_64-efi:
-    binary_name: grubx86.efi
+    binary_name: grubx64.efi
     extra_modules:
       - chain
       - efinet
diff --git a/system-tests/tests/svc-puppet b/system-tests/tests/svc-puppet
index b107075351..7ab69a98d5 100755
--- a/system-tests/tests/svc-puppet
+++ b/system-tests/tests/svc-puppet
@@ -17,7 +17,7 @@ TODO
 EOF
 
 # Act
-curl --output=${tmp}/b http://localhost/cblr/svc/op/puppet/
+curl --output ${tmp}/b http://localhost/cblr/svc/op/puppet/
 
 # Assert
 # FIXME endpoint not yet testable
diff --git a/system-tests/tests/svc-settings b/system-tests/tests/svc-settings
index b47980cfc8..9bfd51efc3 100755
--- a/system-tests/tests/svc-settings
+++ b/system-tests/tests/svc-settings
@@ -212,7 +212,7 @@ cat >${tmp}/a <<-EOF
             ]
         },
         "x86_64-efi": {
-            "binary_name": "grubx86.efi",
+            "binary_name": "grubx64.efi",
             "extra_modules": [
                 "chain",
                 "efinet"
diff --git a/templates/etc/dhcp6.template b/templates/etc/dhcp6.template
index 4e3f766cd8..2a009dd88b 100644
--- a/templates/etc/dhcp6.template
+++ b/templates/etc/dhcp6.template
@@ -35,7 +35,7 @@ option dhcp6.bootfile-url code 59 = string ;
 class "pxeclients" {
     match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
     if substring (option vendor-class-identifier, 15, 5) = "00007" {
-        option dhcp6.bootfile-url "tftp://[$next_server_v6]/grub/grubx86.efi";
+        option dhcp6.bootfile-url "tftp://[$next_server_v6]/grub/grubx64.efi";
     }
     else if substring (option vendor-class-identifier, 15, 5) = "00000" {
         option dhcp6.bootfile-url "tftp://[$next_server_v6]/grub/grub.0";
diff --git a/tests/actions/buildiso_test.py b/tests/actions/buildiso_test.py
index 4b38ae8fa3..664e977ec0 100644
--- a/tests/actions/buildiso_test.py
+++ b/tests/actions/buildiso_test.py
@@ -47,7 +47,7 @@ class TestBuildiso:
     @pytest.mark.parametrize(
         "input_arch,result_binary_name,expected_exception",
         [
-            (enums.Archs.X86_64, "grubx86.efi", does_not_raise()),
+            (enums.Archs.X86_64, "grubx64.efi", does_not_raise()),
             (enums.Archs.PPC, "grub.ppc64le", does_not_raise()),
             (enums.Archs.PPC64, "grub.ppc64le", does_not_raise()),
             (enums.Archs.PPC64EL, "grub.ppc64le", does_not_raise()),
diff --git a/tests/test_data/V3_4_0/settings.yaml b/tests/test_data/V3_4_0/settings.yaml
index 1262f5f118..970f08d45c 100644
--- a/tests/test_data/V3_4_0/settings.yaml
+++ b/tests/test_data/V3_4_0/settings.yaml
@@ -74,7 +74,7 @@ bootloaders_formats:
       - net
       - ofnet
   x86_64-efi:
-    binary_name: grubx86.efi
+    binary_name: grubx64.efi
     extra_modules:
       - chain
       - efinet

grubx86.efi vs grubx64.efi (DHCPv4 -> EFI does not work)
### Either settings.yaml and dhcp6.template are wrong, or dhcp.template, documentation, and tests are wrong

In dhcp.template, cobbler currently references `grubx64.efi` for x86_64 EFI boots, while dhcp6.template references `grubx86.efi` (for the same function). `cobbler mkloaders` generates grubx86.efi based on `settings.yaml` -> `bootloader_formats` -> `x86_64_efi` -> `binary_name` being set to `grubx86.efi`.

This results in the current dhcp.template being unable to use the EFI bootloaders since the filename is wrong.

A quick fix would be to `s/grubx64.efi/grubx86.efi/` in `dhcp.template`, although I would argue the correct way to solve it would be to rename the `binary_name` to grubx64.efi in `settings.yaml`, the `grubx86.efi` in `dhcp6.template` to `grubx64.efi`, and doing whatever needs to be done in `cobbler/settings/migrations` (in which files also reference `grubx86.efi`). This would be in line with other tests and documentation within cobbler (including `mkloaders_test.py`, `cobbler.rst`, `grub.rst`, `system-tests/listings/*/*/*/index`, and generally grubx64.efi being named that way on many Linux distribution EFI directories.

### Steps to reproduce

1. Use ISC DHCP v4 to attempt to boot via PXE on an EFI system
2. Failure due to grubx64.efi not being present on tftp

### Expected behavior

The default templates for DHCP to work out of the box with `cobbler mkloaders`-provided bootloaders

### Cobbler version

<!--- Paste output from `cobbler version` -->
````paste below
cobbler version
Cobbler 3.3.3
  source: b4df3716, Mon Apr 25 13:19:08 2022 +0200
  build time: Fri May  6 13:25:13 2022
````

### Operating system

AlmaLinux 8.5

### Additional information

<!--- Add any other context about the problem here. -->

