#!/bin/sh
""":"
if command -v python3 > /dev/null; then
    exec python3 $0 "$@"
elif command -v python > /dev/null && [ "$(python --version | cut -d' ' -f2 | cut -d. -f1)" = "3" ]; then
    exec python $0 "$@"
fi

echo "error: installer script requires python 3 to run, and it was not found in the environment"

exit 2

":"""
# Install script for Micetro Services
# Copyright (c) 2003-2023 BlueCat http://www.bluecatnetworks.com
# If you encounter any problems running this script
# please contact support@bluecatnetworks.com

import sys
import os
import shutil
import subprocess
import pwd, grp
import platform
import time
import re
import glob


class InstallError(Exception):
    pass


def run_command(command, shell=False):
    """
    Run command "command".

    :param command: The command to run. Should be a list of strings, containing the program and its argument.
    :return: Tuple containing program output and return status code.
    """
    assert isinstance(command, list)
    p = subprocess.Popen(
        command, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
    )
    stout, _ = p.communicate()

    if isinstance(stout, bytes):
        stout = stout.decode(errors="ignore")  # Python 3

    return stout, p.returncode


def run_shell_command(command):
    return run_command([command], shell=True)


def run_command_or_exit(command, error_msg, shell=False):
    # Runs command.  If it returns a non-zero exit code, bail out with error, else return stdout
    out, status = run_command(command, shell)
    if status != 0:
        # Something went wrong... Lets print output from command and exit.
        print(out)
        raise InstallError(
            "%s. Aborting the installation. Please contact support@bluecatnetworks.com."
            % error_msg
            or ("Unable to execute " + " ".join(command))
        )
    return out


def is_user_root():
    """
    Check if program is being run as root.

    :return: True if program is run as root, else false.
    """
    return os.getuid() == 0


def get_platform():
    """
    Get program platform.

    :return: Platform name. For example "linux" or "sunos".
    """
    return platform.system().lower()


def is_ubuntu():
    """
    Check if distribution is ubuntu or debian.

    :return: True if distribution is ubuntu or debian based, else False.

    """
    return os.path.isfile("/etc/debian_version")


def is_redHat():
    """
    Check if distribution is Red Hat or Centos.

    :return: True if distribution is Red Hat or Centos based, else False.

    """
    return os.path.isfile("/etc/redhat-release")


def is_iscBuilt():
    """
    Check if named is an ISC built package

    :return: True if named is an ISC built package, else False.

    """
    return os.path.isdir("/var/opt/isc/scls/isc-bind/named") or os.path.isdir(
        "/var/opt/isc/isc-bind/named"
    )


def is_systemd():
    """
    Check if OS is using systemd.

    :return: True if system is using systemd, else False.
    """
    pgrep_path = "/usr/bin/pgrep"
    if not os.path.exists(pgrep_path):
        raise InstallError(
            "Unable to determine if startup system type is systemd or not."
        )
    # Systemd will run with process id of 1
    _, status = run_shell_command(r"%s systemd | grep ^1\$ > /dev/null" % pgrep_path)

    return status == 0


def is_SELinux_enabled():
    """
    Check if SE Linux is enabled.

    :return: True if SE Linux is enabled, else False.
    """
    getenforce_cmd = "/usr/sbin/getenforce"
    if os.path.exists(getenforce_cmd):
        enforce_level, _ = run_command([getenforce_cmd])
        if str(enforce_level).lower().strip() == "enforcing":
            return True

    return False


def has_user(user):
    """
    Check if user exists.

    :param user: Name of user to check for.
    :return: True if user exists, else False.
    """
    try:
        _ = pwd.getpwnam(user)
        return True
    except KeyError:
        return False


def has_group(group):
    """
    Check if group exists.

    :param group: Name of group to check for.
    :return: True if group exists, else False.
    """
    try:
        _ = grp.getgrnam(group)
        return True
    except KeyError:
        return False


def get_uid(user_name):
    """
    Get uid for a particular user.

    :param user_name: The user to check for.
    :return: The uid for the user, or None if not found.
    """
    try:
        return pwd.getpwnam(user_name).pw_uid
    except KeyError:
        return None


def get_gid(group_name):
    """
    Get gid for a particular group.

    :param group_name: The group to check for.
    :return: The gid for the group, or None if not found.
    """
    try:
        return grp.getgrnam(group_name).gr_gid
    except KeyError:
        return None


def check_installer_content(content_file):
    """
    Check if content listed in "content_file" exists.

    :param content_file: Path to a text file, where each line contains a path to a file that should exist.
    :raises InstallError: if content_file is not found or if content from content_file is missing.
    """
    content = []
    f = None
    try:
        f = open(os.path.join(os.getcwd(), content_file))
        content = f.readlines()
        f.close()
    except:
        if f is not None:
            f.close()
        raise InstallError(
            'Unable to read file "%s" to determine package content.\n' % content_file
        )
    # Check that content exists
    for item in content:
        if not os.path.exists(
            os.path.join(os.getcwd(), item.replace(os.linesep, "").strip())
        ):
            raise InstallError(
                "One or more files appear to be missing from this package.\nPlease re-download it from www.bluecatnetworks.com.\n"
            )


def parse_missing_lib_str(ldd_output):
    """
    Parses output from the ldd command. If ldd complained about missing libs, then
    a list of libraries will be returned. If no missing libs are found, then an empty list is returned.
    :param ldd_output: The output returned from ldd.
    :return: list of missing libraries
    """

    if ldd_output != "":
        lines = ldd_output.split(os.linesep)
        missing_libs = []
        for line in lines:
            if "not found" in line.lower():
                # Line format example: "librt.so.1 => not found"
                missing_libs.append(line.split("=>")[0].strip())
        return missing_libs


def check_lib_dep(binary_path):
    """
    Check missing or wrong libraries for all binaries found in "binary_path".

    :param binary_path: Path to directory where binaries to check dependencies for are stored.
    :raises InstallError: If dependencies for any binary found in "binary_path" is missing.
    """

    if not os.path.exists(binary_path):
        raise InstallError(
            "No install binaries for this platform found. Make sure that a correct installer for this platform has been selected.\n"
        )

    out, _ = run_shell_command(
        'ldd %s | egrep -i "not found" | sort |uniq' % os.path.join(binary_path, "*")
    )
    # Check library dependencies
    missing_libs = parse_missing_lib_str(str(out))
    if missing_libs:
        for missing_lib in missing_libs:
            print("Missing library: %s" % missing_lib)
        raise InstallError(
            "ERROR: Micetro Server Controllers depend on libraries missing from your system:\nPlease contact support@bluecatnetworks.com.\n"
        )

    out, _ = run_shell_command(
        'ldd %s | egrep "\\.so" | sort | uniq' % os.path.join(binary_path, "*")
    )
    if out == "":
        raise InstallError(
            "ERROR: Micetro binaries appear to be incompatible with your system.\nIf this is a 64-bit system, you will need to install 32-bit compatibility\nlibraries before installing Micetro.\n"
        )


def get_startup_dir():
    """
    Get path to startup dir.

    :return: Path to startup dir.
    """
    return "/etc/init.d"


def get_runlevel_dir():
    """
    Get path to run level dir.

    :return: Path to run level dir.
    """
    platform_type = get_platform()
    if platform_type == "sunos":
        return "/etc"
    elif platform_type == "linux":
        if os.path.exists("/etc/rc.d/init.d/functions"):
            return "/etc/rc.d"
        else:
            return "/etc/init.d"
    else:
        raise InstallError('Unsupported platform type "%s\n"' % platform_type)


def has_bind():
    """
    Check if BIND is installed.

    :return: True if Bind is installed, else False.
    """

    if get_alternative_files(["/usr/sbin/named"]) is not None:
        return True

    return is_bind_running()


def is_bind_running():
    """
    Check if BIND is running.

    :return: True if Bind is running, else False.
    """

    out, _ = run_shell_command('ps -ax | grep "/named " | grep -v grep -c')
    running_count = int(str(out).strip())

    return running_count != 0


def has_isc_dhcp():
    """
    Check if ISC dhcpd is installed.

    :return: True if ISC dhcpd is installed, else False.
    """
    return (
        get_alternative_files(["/usr/sbin/dhcpd", "/usr/local/sbin/dhcpd"]) is not None
    )


def has_kea_dhcpv4():
    """
    Check if Kea-dhcpv4 is installed.

    :return: True if Kea-dhcp4 is installed, else False.
    """
    return (
        get_alternative_files(
            [
                "/usr/sbin/kea-dhcp4",
                "/usr/sbin/kea-dhcp4-server",
                "/usr/local/sbin/kea-dhcp4",
            ]
        )
        is not None
    )


def has_apache():
    """
    Check if Apache2 is installed

    :return: True if Apache web server is installed, else False.
    """
    return get_alternative_files(["/usr/sbin/httpd", "/usr/sbin/apache2"]) is not None


def do_pre_install_check():
    """
    Check OS environment and installer. Raise an error if installation can not be made.

    :raises InstallError: If installation can not be made.
    """
    if not is_user_root():
        raise InstallError("You need to have root privileges to run this script.\n")

    if get_platform() not in ["linux", "sunos"]:
        raise InstallError("Micetro is not supported on this platform.\n")

    # Check if installer is being run from installer directory
    installer_path = __file__
    if len(installer_path) >= 2 and installer_path[0:2] == "./":
        installer_path = installer_path[2:]

    if len(installer_path.split(os.pathsep)) > 1:
        raise InstallError(
            "Please cd to the installers directory and run the installer from there.\n"
        )

    check_lib_dep(
        os.path.join(os.getcwd(), "solaris" if get_platform() == "sunos" else "linux")
    )

    check_installer_content(os.path.join(os.getcwd(), "contents.txt"))

    if is_SELinux_enabled():
        msg_type = "Web Services" if is_web_installer() else "Server Controllers"
        raise InstallError(
            "ERROR: SELinux must be disabled or in permissive mode in order to be able to\ninstall Mictro %s."
            % msg_type
        )

    if not is_systemd():
        for directory in [get_runlevel_dir(), get_startup_dir()]:
            if not os.path.exists(directory):
                raise InstallError(
                    "ERROR: Directory %s not found.\nPlease contact support@bluecatnetworks.com\n"
                    % directory
                )


def get_alternative_files(files):
    """
    Return first file that exists, from a list of file paths.

    :param files: A list of file paths to check for.
    :return: First file path from "files" list that exists or "None" if none is found.
    """
    for f in files:
        if os.path.exists(f):
            return f

    return None


def get_alternative_user_group(user_groups, default_user="root", default_group="root"):
    """
    Return first pair of valid (user, group) that exist from "user_group" list.

    :param user_groups: A list of (user, group) tuples to check for. Example: [("named", "named"), ("bind", "bind")]
    :param default_user: Default user to use, if no valid user is found. Defaults to root.
    :param default_group: Default group to use, if no valid group is found. Defaults to root.
    :return: First valid (user, group) from "user_group" or (default_user, default_group) if not found.
    """
    for user, group in user_groups:
        if has_user(user) and has_group(group):
            return user, group

    return default_user, default_group


def kill_program(program_name):
    """
    Stop program with "program_name".

    :param program_name: Name of program to stop.
    """
    if os.path.exists("/usr/bin/killall"):
        run_command(["/usr/bin/killall", program_name, "2>", "/dev/null"])
    elif os.path.exists("/usr/bin/pkill"):
        run_command(["/usr/bin/pkill", "-x", program_name, "2>", "/dev/null"])


def get_systemctl_path():
    """
    Get path to systemctl command.

    :return: Path if found, else fallback to "systemctl".
    """
    systemctl_path = get_alternative_files(["/usr/bin/systemctl", "/bin/systemctl"])
    if systemctl_path is not None:
        return systemctl_path
    else:
        return "systemctl"


def service_control(service, action):
    """
    Stop/start a service.

    :param service: The service to stop.
    :param action: Either "stop", "start" or "restart", depending on action.
    :return: True if action succeeds, else False.
    """
    assert action in ["start", "stop", "restart"]
    status = None
    if is_systemd():
        status = subprocess.call(
            "%s %s %s" % (get_systemctl_path(), action, service),
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
    else:  # init.d
        status = subprocess.call(
            "%s %s" % (os.path.join(get_startup_dir(), service), action),
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

    return status == 0


def enable_service(service_name):
    """
    Enable a given service, so that it runs on system startup.

    :param service_name: Name of service to enable.
    """
    if not is_systemd():
        if is_ubuntu():
            run_command(["update-rc.d", service_name, "defaults", "40", "60"])
        else:
            set_runlevel(
                service_name,
                get_startup_dir(),
                get_runlevel_dir(),
                (3) if get_platform() == "sunos" else (3, 5),
                40,
            )
    else:
        run_command([get_systemctl_path(), "daemon-reload"])
        run_command([get_systemctl_path(), "enable", service_name])


def get_num_runlevels():
    """
    Get number of run levels that exist for current platform.

    :return: The number of run levels to use.
    """
    if get_platform() == "sunos":
        return 3
    else:
        return 6


def set_runlevel(script, script_dir, rc_dir, levels, priority):
    """
    Set run level for a particular service.

    :param script: start script name for a service.
    :param script_dir: Usually, this will be /etc/init.d
    :param rc_dir: Directory where rc scripts are stored (e.g. /etc/rc.d).
    :param levels: Levels that the service should be started in..
    :param priority: priority of the service. Determines startup order relative to other installed services.
    """
    for run_level in range(
        1, get_num_runlevels() + 1
    ):  # 1..6 for Red Hat, 1..3 for Solaris
        run_level_dir = os.path.join(
            rc_dir, "rc%i.d" % run_level
        )  # Example: run_level_dir = /etc/rc.d/rc1.d
        if not os.path.exists(run_level_dir):
            raise InstallError(
                "ERROR: Directory %s not found.\nPlease contact support@bluecatnetworks.com.\n"
                % run_level_dir
            )
        # Cleanup old links
        run_shell_command(
            "rm -f %s/S*%s" % (run_level_dir, script)
        )  # Example: rm -f /etc/rc.d/rc1.d/S*mmcentral
        run_shell_command("rm -f %s/K*%s" % (run_level_dir, script))

        # Install either start or kill script
        if run_level in levels:
            # Service should start in this run level.
            run_shell_command(
                "ln -s %s %s > /dev/null 2> /dev/null"
                % (
                    os.path.join(script_dir, script),
                    os.path.join(run_level_dir, "S%s%s" % (str(priority), script)),
                )
            )
        else:
            # service should stop in this run level
            run_shell_command(
                "ln -s %s %s > /dev/null 2> /dev/null"
                % (
                    os.path.join(script_dir, script),
                    os.path.join(run_level_dir, "K%s%s" % (str(priority), script)),
                )
            )


def get_line_that_starts_with(file_path, starts_with):
    """
    Get first line in file "file_path", that starts with the string "starts_with".
    :param file_path: Path to file that should be parsed.
    :param starts_with: String to check for.
    :return: The line if found, else None.
    """
    f = None
    try:
        f = open(file_path, "r")
        lines = f.readlines()
        f.close()
        for line in lines:
            if line.startswith(starts_with):
                return line.replace(os.linesep, "")
    except:
        if f:
            f.close()

    return None

def check_cmddi_token_existence():
    hub_head_config_path = "/var/mmsuite/hub-head/hub-head.yaml"
    if os.path.exists(hub_head_config_path):
        f = open(hub_head_config_path)
        lines = f.readlines()
        f.close()
        for line in lines:
            if line.strip().startswith('token: '):
                return line.strip().replace('token: ', '')
    return None

def get_bind_chroot():
    """
    Get path to chroot for Bind. Note that results for Red Hat (and Centos) will always be None.
    :return: Path to Bind chroot or None, if Bind is not configured to run in chroot or if path is not found.
    """
    bdds_chroot = "/replicated/jail/named/"
    if is_bdds() and os.path.exists(bdds_chroot):
        return bdds_chroot
    elif is_redHat():
        return None  # Treat jailed bind on red hat like it is not running inside a jail
    elif os.path.exists("/etc/default/bind9"):  # Ubuntu
        # Look for OPTIONS="..."
        options = get_line_that_starts_with("/etc/default/bind9", "OPTIONS=")
        if options is not None:
            # If chroot-ed, bind will be started with the option "-t <chroot>"
            args = options.replace('"', " ").split()
            for i in range(0, len(args)):
                if args[i] == "-t" and i + 1 < len(args):
                    root_dir = args[i + 1]
                    if os.path.exists(root_dir):
                        return root_dir
    return None


def is_bind_in_chroot():
    """
    Check if Bind is running in a chroot.
    :return: True if Bind is running in a chroot, else False. Note that for Red Hat/Centos, the results are always False.
    """
    return get_bind_chroot() is not None


def is_named_conf_arranged(named_conf_path):
    """
    Check if Bind config has been arranged.
    :param named_conf_path: Path to named.conf file.
    :return: True if Bind config has been arranged, else False.
    """
    return (
        get_line_that_starts_with(
            named_conf_path, "// This file was generated by the Men & Mice Suite"
        )
        is not None
        or get_line_that_starts_with(
            named_conf_path, "// This file was generated by Micetro"
        )
        is not None
    )


def is_apache_running():
    """
    To be sure all Apache config is where we expect it to be we need to make sure
    it has been started at least once. Therefor we require it to be running before
    installation.

    :return: True if Apache web server is running.
    """
    apache_location = get_alternative_files(["/usr/sbin/apache2", "/usr/sbin/httpd"])
    if apache_location:
        command = ("ps -ax | grep %s  | grep -v grep -c") % apache_location
        out, _ = run_shell_command(command)
        running_count = int(str(out).strip())
        return running_count != 0
    return False


def is_bdds():
    return os.path.exists("/usr/local/bluecat/version.dat") and os.path.exists(
        os.path.join(os.getcwd(), "bdds")
    )


def run_bdds_setup_script(script_name):
    cwd = os.getcwd()
    try:
        os.chdir(os.path.join(cwd, "bdds"))
        run_command_or_exit(
            ["./" + script_name], "An error came up while setting up the BDDS appliance"
        )
    finally:
        os.chdir(cwd)


class Installer(object):
    """
    Base class for implementing an installer for a particular component (e.g. mmcentral, mmremote, etc).
    """

    def __init__(self, service_name, full_service_name, port, settings={}):
        """
        Name of service that the installer is for.

        :param installer_name: Name of installer
        :param service_name: Name of service.
        :param port: Port that service listens to.
        """
        self.service_name = service_name
        self.full_service_name = full_service_name
        self.port = port
        self.service_data = {}
        self.files = []
        self.directories = []
        self.copy_dirs_info = []
        self.remove_items = []
        self.config_line_items = []
        self.is_verbose = True
        self.is_prepared = False
        self.settings = settings

        binary_src = ""
        binary_dst = ""
        binary_name = service_name + "d" if service_name != "hub-head" else service_name
        binary_src = os.path.join(
            os.getcwd(), self._get_bin_src_dir(), binary_name
        )
        binary_dst = os.path.join("/usr/sbin", binary_name)

        if self.is_a_service():
            self.add_file_info(
                binary_src,
                binary_dst,
                "root",
                "root",
                "755",
                msg="Installing binary %s to %s." % (binary_src, binary_dst),
            )
        self._prepare_install()

    def _get_bin_src_dir(self):
        """
        Get name of directory containing binaries to install.
        :return: Name of directory containing binaries.
        """
        if get_platform() == "sunos":
            return "solaris"
        else:
            return "linux"

    def _prepare_install(self):
        """
        Perform work needed to prepare for installation.
        Should NOT do any actual modifications to the OS.
        """
        if self.is_prepared:
            return

        self.check_environment()
        self.prepare_config()
        self.prepare_startup_config()
        self.prepare_defaults_config()
        self.prepare_data_directories()

        self.is_prepared = True

    def do_install(self):
        """
        Do the actual installation.
        """
        if not self.is_prepared:
            raise InstallError(
                "Installer not prepared. Need to successfully run the prepare_install method, before do_install can be invoked."
            )

        self.print_msg(" ")
        self.print_msg("Running installer for %s" % self.full_service_name)
        self.stop_service()
        self.write_config()
        self.arrange_config()
        self.register_service()
        self.start_service()

    def check_environment(self):
        """
        Do any pre-checks needed on the system environment, to make sure that installation
        for a particular component can be made. Should raise an InstallError if installation can not be made.
        """
        pass

    def prepare_config(self):
        """
        Write any parameters needed into "service_data".
        """
        pass

    def prepare_data_directories(self):
        """
        Add list of files and directories to create or copy during install.
        """
        pass

    def arrange_config(self):
        """
        Modify any existing configuration during installation.
        """
        pass

    def get_systemd_template_path(self):
        """
        Get path to systemd service template. By default, the system template <service_name>.service found in
        "startupscripts[/ubuntu]/" is used. Overwrite this method to return a different path.
        The template will be sprinted with values found in the "service_config" dictionary.

        :return: Path to systemd template file.
        """
        if is_ubuntu():
            return os.path.join(
                os.getcwd(),
                "startupscripts",
                "ubuntu",
                "%s.service" % self.service_name,
            )
        else:
            return os.path.join(
                os.getcwd(), "startupscripts", "%s.service" % self.service_name
            )

    def get_initd_template_path(self):
        """
        Get path to init.d start template. By default, the template  <service_name>.init, found in
        "startupscripts[/ubuntu]/" is used. Overwrite this method to return a different path.
        The template will be sprinted with values found in the "service_config" dictionary.

        :return: Path to init startup template to use.
        """
        if is_ubuntu():
            return os.path.join(
                os.getcwd(), "startupscripts", "ubuntu", "%s.init" % self.service_name
            )
        else:
            return os.path.join(os.getcwd(), "startupscripts", self.service_name)

    def get_service_user_group(self):
        """
        Get (user, group) that the service should run as.

        :return: Tuple containing (user, group), that service should run as.
        """
        return "root", "root"

    def print_msg(self, msg):
        """
        Prints message "msg", if the verbosity is set to True and if msg is not None or the empty string.
        :param msg: The message to print.
        """
        if self.is_verbose and msg is not None and msg != "":
            print(msg)

    def get_settings(self):
        """
        Get installer settings provided by user during installation.
        :return:
        """
        return self.settings

    def chmod(self, file, priv, recursive=False):
        cmd = ["chmod", priv, file]
        if recursive:
            cmd.insert(1, "-R")
        run_command_or_exit(
            cmd, "Unable to set access privileges for %s" % file, shell=False
        )

    def chown(self, file, owner, group, recursive=False):
        self.print_msg("Setting owner of %s" % file)
        cmd = ["chown", (owner + ":" + group), file]
        if recursive:
            cmd.insert(1, "-R")
        run_command_or_exit(cmd, "Unable to change owner of %s" % file, shell=False)

    ########################################################################################################################

    def prepare_startup_config(self):
        """
        Create service startup files.

        """
        if not self.is_a_service():
            return

        if is_systemd():
            dst = os.path.join(
                self.get_systemd_root_path(), "%s.service" % self.service_name
            )
            self.add_file_info(
                self.get_systemd_template_path(),
                dst,
                "root",
                "root",
                "644",
                ("Creating systemd unit file %s." % dst),
                self.get_service_data(),
            )

        else:
            dst = os.path.join("/", "etc", "init.d", self.service_name)
            self.add_file_info(
                self.get_initd_template_path(),
                dst,
                "root",
                "root",
                "755",
                ("Creating initd startup script %s." % dst),
                self.get_service_data(),
            )

        dir_name = self.service_name + "d" if self.service_name != 'hub-head' else self.service_name
        user, group = self.get_service_user_group()
        self.add_directory_info(
            os.path.join("/var/run", dir_name),
            user,
            group,
            "755",
            "Creating directory %s in /var/run." % (dir_name),
        )

    def prepare_defaults_config(self):
        """
        Create /etc/default/<service_name> on debian based systems.
        Uses startupscripts/ubuntu/<service_name>.default template as a template.
        Data found in the "get_service_data" member function will be used to sprint the template.

        """
        if is_ubuntu() and self.is_a_service():
            defaults_dst = os.path.join("/", "etc", "default", self.service_name)
            self.add_file_info(
                os.path.join(
                    os.getcwd(),
                    "startupscripts",
                    "ubuntu",
                    ("%s.default" % self.service_name),
                ),
                defaults_dst,
                "root",
                "root",
                "644",
                ("Creating default configuration file %s." % defaults_dst),
                self.get_service_data(),
            )

    def write_config(self):
        """
        * Write files added with "add_file_info(...)".
        * Create directories added with "add_directory_info(...)".
        * Copy directories added with "copy_dir()".
        * Rename things that need to be renamed
        * Append to configuration files (only used for apache includes so far)
        """
        for directory_info in self.directories:
            self.write_directory_info(directory_info)

        for file_info in self.files:
            self.write_file_info(file_info)

        self.remove_file_items()
        self.copy_dirs()
        for config_line_item in self.config_line_items:
            self.write_config_line_item(config_line_item)

    def add_file_info(
        self, src, dst, user, group, permissions, msg=None, sprint_data=None
    ):
        """
        Add information about a file to copy during installation.

        :param src: Source path of file to copy.
        :param dst: Destination path of file to copy.
        :param user: User ownership for the file.
        :param group: Group ownership for the file.
        :param permissions: Permissions to set on the file.
        :param msg: Message to write when file is created.
        :param sprint_data: Optional data to sprint the file.
        """
        if not os.path.exists(src):
            raise InstallError("No such file: %s" % src)
        self.files.append(
            {
                "src": src,
                "dst": dst,
                "user": get_uid(user),
                "group": get_gid(group),
                "permissions": permissions,
                "msg": msg,
                "sprint_data": sprint_data,
            }
        )

    def write_file_info(self, file_info):
        """
        Write file from file_info. Note that the method should not be used directly. Instead, files to add should be
        added using the "add_file_info(...)" method.

        :param file_info: Information about the file that should be written.
        """
        if file_info.get("msg", "") != "":
            self.print_msg(file_info["msg"])
        else:
            self.print_msg("Installing file %s" % file_info["dst"])
        template_file = None
        dst_file = None
        try:
            if file_info["sprint_data"] is None or file_info["sprint_data"] == {}:
                # copy the file
                shutil.copy(file_info["src"], file_info["dst"])
            else:
                template_file = open(file_info["src"], "r")
                template = template_file.read()
                template_file.close()
                # Sprint file content with data
                for key in list(file_info["sprint_data"].keys()):
                    template = template.replace(key, file_info["sprint_data"][key])

                # Write file:
                dst_file = open(file_info["dst"], "w")
                dst_file.write(template)
                dst_file.close()

            # Set user, group and permissions
            self.print_msg("Setting access privileges for %s" % file_info["dst"])
            os.chown(file_info["dst"], file_info["user"], file_info["group"])
            self.chmod(file_info["dst"], file_info["permissions"])
        except Exception as e:
            if template_file:
                template_file.close()
            if dst_file:
                dst_file.close()
            raise InstallError("Unable to write file %s." % file_info["dst"])

    def add_directory_info(self, dst, user_name, group_name, permissions, msg=""):
        """
        Add info about a directory that should be created during installation.

        :param dst: Where to create the directory.
        :param user_name: User owner for the directory.
        :param group_name: Group owner for the directory.
        :param permissions: Permissions to set on the directory.
        :param msg: Message to display when the directory is created.
        """
        self.directories.append(
            {
                "dst": dst,
                "user": user_name,
                "group": group_name,
                "permissions": permissions,
                "msg": msg,
            }
        )

    def write_directory_info(self, directory_info):
        if directory_info.get("msg", None) is None:
            self.print_msg("Creating directory %s" % directory_info["dst"])
        else:
            self.print_msg(directory_info["msg"])
        try:
            os.makedirs(directory_info["dst"])
            self.chmod(directory_info["dst"], directory_info["permissions"])
        except:
            if not os.path.exists(directory_info["dst"]):
                raise InstallError(
                    "Unable to create directory %s." % directory_info["dst"]
                )
        self.print_msg("Setting access privileges for %s" % (directory_info["dst"]))
        os.chown(
            directory_info["dst"],
            get_uid(directory_info["user"]),
            get_gid(directory_info["group"]),
        )

    def add_copy_directory(self, src, dst, user_name, group_name, permissions, msg):
        """
        Add informations about a directory that should be copied during installation.

        :param src: Soure path for directory.
        :param dst: Destination path for directory.
        :param user_name: User ownership for the directory.
        :param group_name: Group ownership for the directory.
        :param permissions: Permissions to set on the directory
        :param msg: Message to display when the directory is copied.
        """
        self.copy_dirs_info.append(
            {
                "src": src,
                "dst": dst,
                "user": user_name,
                "group": group_name,
                "permissions": permissions,
                "msg": msg,
            }
        )

    def add_item_to_remove(self, src, msg=None):
        """
        Add information about a file system item that needs to be removed before creation or copying.
        Nothing is deleted until we enter the installation phase.

        :param src: Soure path for item.
        :param dst: Destination path for item.
        :param msg: Message to display when the item is removed.
        """
        self.remove_items.append({"src": src, "msg": msg})

    def add_config_line_item(self, line, dst, msg=None):
        """
        Add single line to a configuration file if it is not already there and the file exists.
        No file is altered until we reach the isntallation phase

        :param line: Configuration line to append to file.
        :param dst: Path for the config file to search for item.
        :param msg: Message to display when the item is added.
        """
        self.config_line_items.append({"line": line, "dst": dst, "msg": msg})

    def remove_file_items(self):
        """
        Remove files from remove_items. Note that the method should not be used directly. Instead, files to remove should be
        added using the "add_item_to_remove(...)" method.
        """
        for remove_item in self.remove_items:
            if remove_item.get("msg"):
                self.print_msg(remove_item.get("msg"))
            rm_cmd = "rm -r -f %s" % (remove_item["src"])
            run_shell_command(rm_cmd)

    def write_config_line_item(self, config_line_item):
        """
        Opens a file, reads it to memory. Checks if the line exists and if not writes it, and rewrites the file.
        The bootstrapping functions should have  already made exist checks and verification before attempting this.
        One should NOT call this function directly.
        """
        if config_line_item.get("msg"):
            self.print_msg(config_line_item.get("msg"))
        line = config_line_item.get("line")
        dst = config_line_item.get("dst")
        with open(dst, "r+") as f:
            for config_line in f:
                if line in config_line:
                    break
            else:  # not found, we are at the eof
                f.write("\n# Inserted by a Micetro installer script.\n")
                f.write(line)
                f.write("\n")

    def sprint_to_tmp_file(self, filename, lines):
        """
        Sprints lines to a new file named (filename) in the current working directory and
        then marks that file for deletion. Usefull when you have to persist user input.
        This way we can take use of all changes taking place at once by using add_file_info
        and keep the installer folder valid for a re-run. Be careful since the file is truncated
        if it exists.
        returns the path to the temporary file created
        """
        tmp = os.path.join(os.getcwd(), filename)
        with open(tmp, "w") as f:
            f.write("\n".join(lines))
            f.close()
        self.add_item_to_remove(tmp, ("Removing temporary file %s" % filename))
        return tmp

    def copy_dirs(self):
        for copy_dir in self.copy_dirs_info:
            self.print_msg(copy_dir.get("msg"))
            cp_cmd = ["cp", "-r", "-f"] + glob.glob(copy_dir["src"]) + [copy_dir["dst"]]
            run_command_or_exit(
                cp_cmd, ("Unable to copy %s to %s" % (copy_dir["src"], copy_dir["dst"]))
            )

            # Set user, group and permissions
            self.chown(copy_dir["dst"], copy_dir["user"], copy_dir["group"], True)
            self.chmod(copy_dir["dst"], copy_dir["permissions"], True)

            # Make sure that directory access is set for user and group
            self.chmod(copy_dir["dst"], "ug+X", True)

    def get_service_data(self):
        """
        Get the service data property
        """
        return self.service_data

    def set_service_data(self, service_data):
        """
        Set the service data propterty
        """
        self.service_data = service_data

    def get_systemd_root_path(self):
        """
        Get path to directory systemd startup files are stored.

        :return: Path to systemd root path, or None if not found.
        """
        locations = [
            "/lib/systemd/system",
            "/etc/systemd/system",
            "/run/systemd/system",
        ]
        for location in locations:
            if os.path.exists(location):
                return location

        return None

    def set_verbose(self, is_verbose):
        """
        Display additional information during install. Defaults to False.
        :param is_verbose: True if additional information should be displayed during install, else False.
        """
        self.is_verbose = is_verbose

    def register_service(self):
        """
        Set run level and enable service.
        """
        if self.is_a_service():
            enable_service(self.service_name)

    def stop_service(self):
        """
        Stop the service.
        """
        if not self.is_a_service():
            return
        if os.path.exists(os.path.join(get_startup_dir(), self.service_name)) or (
            self.get_systemd_root_path() is not None
            and os.path.exists(
                os.path.join(
                    self.get_systemd_root_path(), "%s.service" % self.service_name
                )
            )
        ):
            self.print_msg("Stopping service %s." % self.service_name)
            service_control(self.service_name, "stop")
        # Send a signal, just in case.
        kill_program("%sd" % self.service_name)

    def start_service(self):
        """
        Start the service.
        """
        if self.is_a_service():
            self.print_msg("Starting service %s." % self.service_name)
            service_control(self.service_name, "start")

    def get_port(self):
        """
        Get port that service is listening to.
        """
        return self.port

    def add_preference_value(self, preferences_file, preference_name, preference_value):
        read_file = None
        write_file = None
        old_content_lines = []
        new_content_lines = []
        try:
            if os.path.exists(preferences_file):
                read_file = open(preferences_file, "r")
                old_content_lines = read_file.readlines()
                read_file.close()
            for line in old_content_lines:
                # Save all preference values except the one we are adding
                if preference_name not in line:
                    new_content_lines.append(line)
            new_content_lines.append(
                '<%s value="%s"/>%s' % (preference_name, preference_value, os.linesep)
            )
            write_file = open(preferences_file, "w")
            write_file.writelines(new_content_lines)
            write_file.close()
        except:
            if read_file is not None:
                read_file.close()
            if write_file is not None:
                write_file.close()
            raise InstallError(
                "Unable to add preference value for %s" % preference_name
            )

    def is_a_service(self):
        """
        Returns true if installer is installing a service (deamon). True for everything except the web interface.
        :return boolean that indicates whether a deamon installer or not
        """
        if self.service_name in [
            "mmcentral",
            "mmremote",
            "mmdhcpremote",
            "mmws",
            "mmupdater",
            "hub-head",
        ]:
            return True
        return False


class CentralInstaller(Installer):
    """
    Install class for Micetro Central.
    """

    def __init__(self, settings={}):
        Installer.__init__(self, "mmcentral", "Central", 1231, settings=settings)

    def prepare_config(self):
        self.service_data = {
            "_USER_": "root",
            "_GROUP_": "root",
            "_PATH_": "/usr/sbin",
            "_DATA_": "/var/mmsuite/mmcentral",
            "_PARAM1_": " -u root -g root -d /var/mmsuite/mmcentral",
            "_LOCATION_": "/usr/sbin",
        }

    def prepare_data_directories(self):
        self.add_directory_info("/var/mmsuite/mmcentral/update", "root", "root", "755")
        self.add_copy_directory(
            os.path.join(os.getcwd(), "update", "*"),
            "/var/mmsuite/mmcentral/update",
            "root",
            "root",
            "640",
            "Installing update binaries in /var/mmsuite/mmcentral/update.",
        )
        # extensions/
        extensions_dir = os.path.join(self.service_data["_DATA_"], "extensions")
        if not os.path.isdir(extensions_dir):
            self.add_directory_info(extensions_dir, "root", "root", "755")
        self.add_copy_directory(
            os.path.join(os.getcwd(), "extensions", "*"),
            extensions_dir,
            "root",
            "root",
            "755",
            ("Installing extension scripts in %s." % extensions_dir),
        )
        # scripts/
        scripts_dir = os.path.join(self.service_data["_DATA_"], "scripts")
        if not os.path.isdir(scripts_dir):
            self.add_directory_info(scripts_dir, "root", "root", "755")
        self.add_copy_directory(
            os.path.join(os.getcwd(), "scripts", "*"),
            scripts_dir,
            "root",
            "root",
            "755",
            ("Initializing scripts folder: %s." % scripts_dir),
        )


class UpdaterInstaller(Installer):
    """
    Install class for Micetro Update agent.
    """

    def __init__(self, settings={}):
        Installer.__init__(self, "mmupdater", "Update agent", 4603, settings=settings)
        self.installed_conf = ""

    def prepare_config(self):
        self.service_data = {
            "_USER_": "root",
            "_GROUP_": "root",
            "_PATH_": "/usr/sbin",
            "_PARAM1_": "",
            "_LOCATION_": "/usr/sbin",
        }

    def prepare_data_directories(self):
        self.add_directory_info("/var/mmsuite/updater", "root", "root", "755")


class ApacheDependantInstaller(Installer):
    """
    Install class for service that rely on apache2
    """

    def __init__(self, service, full_service_name, port, settings={}):
        Installer.__init__(self, service, full_service_name, port, settings=settings)

    def check_environment(self):
        """
        Verifies the apache installation and relevant paths.
        Based on information found here: https://wiki.apache.org/httpd/DistrosDefaultLayout
        """

        if not has_apache():
            raise InstallError(
                "Unable to install a web service. No Apache installation detected."
            )
        root_dir = self.get_apache_root_dir()
        if root_dir is None or not os.path.isdir(root_dir):
            raise InstallError(
                "Unable to install a webservice. No Apache root dir found."
            )
        conf_dir = self.get_apache_config_dir()
        if conf_dir is None or not os.path.isdir(conf_dir):
            raise InstallError(
                "Unable to install a web service. No Apache configuration found."
            )
        conf_file = self.get_apache_config_file()
        if conf_dir is None or not os.path.isfile(conf_file):
            raise InstallError(
                "Unable to install a web service. No Apache configuration file found."
            )
        doc_dir = self.get_apache_document_root()
        if doc_dir is None or not os.path.isdir(doc_dir):
            raise InstallError(
                "Unable to install a web service. No Apache data directory detected."
            )
        if (
            self.settings["web-application"]
            and not self.settings["web-virtual-host-domain"]
        ):
            web_app_config = self.get_web_app_config()
            if web_app_config and len(web_app_config) == 2:
                self.settings["web-virtual-host-domain"] = web_app_config[0]
            else:
                # should never happen, since if not supplied or already confirmed to be configured on machine we would have aborted already
                raise InstallError(
                    "Unable to install Web application. Unable to determine a virtual host domain name to use for the installation. Please supply one."
                )
        if (
            self.settings["web-application"]
            and self.settings["web-virtual-host-domain"]
        ):
            web_app_config = self.get_web_app_config()
            if web_app_config and len(web_app_config) == 2 and self.settings["web-virtual-host-domain"] != web_app_config[0] and not self.settings["force"]:
                raise InstallError(
                    f"""
Unable to install Web application.

Virtual host domain of the previously installed web application {web_app_config[0]} differs from the virtual host
domain provided in the command line: {self.settings["web-virtual-host-domain"]}.

If you are sure you want to change the virtual host domain and modify the apache2 configuration file mmweb.conf by
hand, please add the --force argument to the installer.
"""
                )
            # A simple validation, should be usable as a domain name and as a folder name in document root
            regex = r"[^\w.-]+"  # match none word chars except dots and hyphen
            all_legal_chars = re.search(regex, self.settings["web-virtual-host-domain"])
            approot = os.path.join(
                self.get_apache_document_root(),
                self.settings["web-virtual-host-domain"],
            )
            if (
                not approot.startswith(self.get_apache_document_root())
                or self.settings["web-virtual-host-domain"].startswith("-")
                or self.settings["web-virtual-host-domain"].endswith("-")
                or all_legal_chars is not None
            ):
                # This can be raised here since no work has been done on the machine.
                raise InstallError(
                    "Unable to install Web application. '%s' is not valid as a virtual host domain name."
                    % self.settings["web-virtual-host-domain"]
                )

    def get_apache_root_dir(self):
        """
        Locate apache root directory
        """
        return get_alternative_files(["/etc/apache2/", "/etc/httpd"])

    def get_apache_config_dir(self):
        """
        Locate apache conf dir
        """
        return get_alternative_files(["/etc/apache2", "/etc/httpd", "/etc/httpd/conf"])

    def get_apache_config_file(self):
        """
        Locate apache configuration file
        """
        paths = [
            "/etc/apache2/apache2.conf",  # Ubuntu
            "/etc/apache2/httpd.conf",  # SuSE
            "/etc/httpd/conf/httpd.conf",  # Fedora, CentOS, RHEL
        ]
        return get_alternative_files(paths)

    def get_apache_document_root(self):
        return get_alternative_files(
            [
                "/var/www",
                "/var/www/html",
                "/var/apache2/htdocs",
                "/srv/www/htdocs",
            ]
        )

    def get_apache_modules_dir(self):
        modules = [
            "/usr/lib/apache2/modules",
            "/usr/lib/httpd/modules",
            "/usr/lib64/httpd/modules",
            "/usr/lib64/apache2",
        ]
        return get_alternative_files(modules)

    def get_apache_error_log(self):
        # some disto installations sym link from etc/httpd/logs to /var/logs/...
        logs = [
            "/var/log/httpd/error_log",
            "/var/log/apache2/error_log",
            "/var/log/apache2/error.log",
            "/etc/httpd/logs/error_log",
        ]
        path = get_alternative_files(logs)
        if not path:
            raise InstallError(
                "Unable to determine location of Apache error log file. Please make sure it exists."
            )
        return path

    def get_apache_access_log(self):
        # some disto installations sym link from etc/httpd/logs to /var/logs/...
        logs = [
            "/var/log/httpd/access_log",
            "/var/log/apache2/access_log",
            "/var/log/apache2/access.log",
            "/etc/httpd/logs/access_log",
        ]
        path = get_alternative_files(logs)
        if not path:
            raise InstallError(
                "Unable to determine location of Apache access log file. Please make sure it exists."
            )
        return path

    def set_installed_apache_config(self, file_path):
        self.installed_conf = file_path

    def get_installed_apache_config(self):
        return self.installed_conf

    def get_apache_user_and_group(self):
        # TODO: Find out if I need be using www-data as backup
        return get_alternative_user_group([("www-data", "www-data"), ("root", "root")])

    def apache_restart_message(self):
        self.print_msg(" ")
        self.print_msg(
            "Please restart the Apache web service for the changes to take effect."
        )

    def get_web_app_dir(self):
        return "/var/mmsuite/web_services/"

    def get_web_app_config_path(self):
        return self.get_web_app_dir() + "web_app_configuration"

    def get_web_app_upgrade_script(self):
        return self.get_web_app_dir() + "upgrade_web_app.sh"

    def get_web_app_config(self):
        # if the file exists it should contain two lines, the dns virtual host name and the path to the data dir
        # typically /var/www/the.virtual.host.name
        if os.path.isfile(self.get_web_app_config_path()):
            with open(self.get_web_app_config_path(), "r") as f:
                lines = f.readlines()
            f.close()
            return [x.rstrip() for x in lines]
        return None


class WebServiceInstaller(ApacheDependantInstaller):
    """
    Install class for the Micetro Web service (API services).
    """

    def __init__(self, settings={}):
        self.mmws_port = 8111
        ApacheDependantInstaller.__init__(
            self, "mmws", "Web Service", self.mmws_port, settings=settings
        )

    def prepare_config(self):
        user, group = self.get_service_user_group()
        datadir = "/var/mmsuite/web_services"
        self.service_data = {
            "_USER_": user,
            "_GROUP_": group,
            "_PORT_": str(self.mmws_port),
            "_PATH_": "/usr/sbin",
            "_DATA_": datadir,
            "_DATADIR_": datadir,
            "_LOCATION_": "/usr/sbin",
            "_MODULES_": self.get_apache_modules_dir(),
            "_PARAM1_": "-u %s -g %s -d %s -ll3 -p %d -http"
            % (user, group, datadir, self.mmws_port),
        }

    def prepare_data_directories(self):
        user, group = (
            self.get_service_user_group()
        )  # check if this is same  mmws or apache users
        # Where to store the rest docs
        rest_doc_dir = os.path.join(self.service_data["_DATA_"], "doc")
        # Location of the mmws apache configuration file
        webext_dir = os.path.join(self.get_apache_config_dir(), "conf")
        self.set_installed_apache_config(os.path.join(webext_dir, "mmws.conf"))
        self.add_directory_info(rest_doc_dir, user, group, "755")
        if not os.path.isdir(webext_dir):
            self.add_directory_info(webext_dir, user, group, "755")
        if not os.path.isdir(rest_doc_dir):
            self.add_directory_info(rest_doc_dir, user, group, "755")
        self.add_copy_directory(
            os.path.join(os.getcwd(), "rest-doc", "*"),
            rest_doc_dir,
            user,
            group,
            "755",
            ("Installing REST documentation files in %s." % rest_doc_dir),
        )
        if not os.path.exists(os.path.join(self.get_apache_root_dir(), "conf")):
            self.add_directory_info(
                os.path.join(self.get_apache_root_dir(), "conf"), user, group, "775"
            )
        if get_alternative_files([self.get_installed_apache_config()]) is None:
            self.add_file_info(
                os.path.join(os.getcwd(), "mmws.conf.in"),
                self.get_installed_apache_config(),
                user,
                group,
                "644",
                "Installing apache config.",
                self.service_data,
            )
        config_include = "Include %s" % self.get_installed_apache_config()
        self.add_config_line_item(
            config_include,
            self.get_apache_config_file(),
            "Adding include in main apache configuration.",
        )


class WebInterfaceInstaller(ApacheDependantInstaller):
    """
    Install class for Micetro Web Interface.
    """

    def __init__(self, settings={}):
        ApacheDependantInstaller.__init__(
            self, "mmwebapp", "Web Application", 80, settings=settings
        )

    def prepare_config(self):
        self.service_data = {
            "_USER_": "root",
            "_GROUP_": "root",
            "_LOCATION_": "",
            "_DOMAIN_": self.settings["web-virtual-host-domain"],
            "_ERRORLOG_": self.get_apache_error_log(),
            "_ACCESSLOG_": self.get_apache_access_log(),
            "_APPROOT_": os.path.join(
                self.get_apache_document_root(),
                self.settings["web-virtual-host-domain"],
            ),  # Already validated in the ApacheDepentantInstaller
            "_MODULES_": self.get_apache_modules_dir(),
        }

        self.apache_config_file = os.path.join(self.get_apache_config_dir(), "conf")

    def prepare_data_directories(self):
        UPGRADE_SCRIPT = """#!/bin/bash
# This script is used to upgrade the Micetro Web Application code.  It is executed with a directory
# containing a new version of the code as its only parameter.  If you want to change the configuration of
# the web application, add these changes here and they will be reapplied on every upgrade
if [ ! -d {_APPROOT_} ]
then
    echo "Unable to find web application at {_APPROOT_}"
    exit 1
fi
if [ ! -d $1 ]
then
    echo "Usage: upgrade_web_app.sh update_dir"
    exit 1
fi
rm -rf -- {_APPROOT_}
mv $1 {_APPROOT_}
chown -R {_USER_}:{_GROUP_} {_APPROOT_}
"""
        # note: We just ignore if there exists an old MenAndMice directory. Who are we to say you cant run both?
        user, group = self.get_apache_user_and_group()
        webext_dir = os.path.join(self.get_apache_config_dir(), "conf")
        self.set_installed_apache_config(os.path.join(webext_dir, "mmweb.conf"))
        self.add_directory_info(self.service_data["_APPROOT_"], user, group, "755")
        self.add_copy_directory(
            os.path.join(os.getcwd(), "web", "*"),
            self.service_data["_APPROOT_"],
            user,
            group,
            "755",
            "Copy web interface client files to data directory",
        )
        if not os.path.exists(os.path.join(self.get_apache_root_dir(), "conf")):
            self.add_directory_info(
                os.path.join(self.get_apache_root_dir(), "conf"), user, group, "775"
            )
        # Check if we already installed before sprinting over possibly user modified file.
        if get_alternative_files([self.get_installed_apache_config()]) is None:
            self.add_file_info(
                os.path.join(os.getcwd(), "mmweb.conf.in"),
                self.get_installed_apache_config(),
                user,
                group,
                "644",
                "Configuring a virtual host for domain.",
                self.service_data,
            )
        web_config_in = self.sprint_to_tmp_file(
            "web_app_configuration.in",
            [self.service_data["_DOMAIN_"], self.service_data["_APPROOT_"]],
        )
        if not os.path.exists(self.get_web_app_dir()):
            self.add_directory_info(
                self.get_web_app_dir(), "root", "root", "775"
            )
        self.add_file_info(
            web_config_in,
            self.get_web_app_config_path(),
            "root",
            "root",
            "644",
            "Adding reference file to support automatic update of web application.",
            self.service_data,
        )
        upgrade_script = UPGRADE_SCRIPT.format(
            **{
                "_APPROOT_": self.service_data["_APPROOT_"],
                "_USER_": user,
                "_GROUP_": group,
            }
        )
        upgrade_script_file = self.sprint_to_tmp_file(
            "upgrade_web_app.sh.in", [upgrade_script]
        )
        self.add_file_info(
            upgrade_script_file,
            self.get_web_app_upgrade_script(),
            "root",
            "root",
            "755",
            "Adding script to update web application.",
            self.service_data,
        )

        config_include = "Include %s" % self.get_installed_apache_config()
        self.add_config_line_item(
            config_include,
            self.get_apache_config_file(),
            "Adding include in main apache configuration.",
        )

class HubHeadInstaller(Installer):
    def __init__(self, settings={}):
        Installer.__init__(self, "hub-head", "Hub Head", 8700, settings=settings)

    def prepare_config(self):
        token = self.settings["token"]

        self.service_data = {
            "_USER_": "root",
            "_GROUP_": "root",
            "_CONF_": "/var/mmsuite/hub-head/hub-head.yaml",
            "_PATH_": "/usr/sbin",
            "_LOCATION_": "/usr/sbin",
            "_DATA_": "/var/mmsuite/hub-head",
            "_PARAM1_": "--config-path /var/mmsuite/hub-head/hub-head.yaml --addr 127.0.0.1:8700",
            "_AGENT_TOKEN_": token
        }

    def prepare_data_directories(self):
        self.add_directory_info("/var/mmsuite/hub-head", "root", "root", "755")
        self.add_file_info(
            os.path.join(os.getcwd(), "hub-head", "hub-head.yaml"),
            "/var/mmsuite/hub-head/hub-head.yaml",
            "root", "root", "755",
            "Creating hub-head configuration file",
            self.get_service_data()
        )

class BaseDHCPRemoteInstaller(Installer):
    """
    Install class for Micetro standalone DHCP server controller and base class for other DHCP server controllers
    """

    def __init__(self, settings={}):
        Installer.__init__(self, "mmdhcpremote", "DHCP Remote", 4151, settings=settings)

    def prepare_config(self):
        user, group = self.get_service_user_group()
        self.service_data = {
            "_USER_": user,
            "_GROUP_": group,
            "_CONF_": "",
            "_PATH_": "/usr/sbin",
            "_LEASE_": "",
            "_LOCATION_": "/usr/sbin",
            "_DATA_": "/var/mmsuite/dhcp_server_controller",
            "_PARAM1_": "-u %s -g %s -d /var/mmsuite/dhcp_server_controller"
            % (user, group),
        }

    def prepare_data_directories(self):
        user, group = self.get_service_user_group()
        self.add_directory_info(
            "/var/mmsuite/dhcp_server_controller", user, group, "755"
        )

class ISCRemoteInstaller(BaseDHCPRemoteInstaller):
    """
    Install class for Micetro ISC DHCP server controller.
    """

    def __init__(self, settings={}):
        BaseDHCPRemoteInstaller.__init__(self, settings=settings)

    def check_environment(self):
        if not has_isc_dhcp():
            raise InstallError(
                "Unable to install ISC DHCP server controller. No ISC dhcp installation detected."
            )

        config_file = self.get_dhcpd_conf_path()
        if config_file is None or not os.path.isfile(config_file):
            raise InstallError(
                "Unable to install ISC DHCP server controller. No config file detected."
            )

        lease_file = self.get_lease_file_path()
        if lease_file is None or not os.path.isfile(lease_file):
            raise InstallError(
                "Unable to install ISC DHCP server controller. No lease file detected."
            )

    def prepare_config(self):
        user, group = self.get_service_user_group()
        self.service_data = {
            "_USER_": user,
            "_GROUP_": group,
            "_CONF_": self.get_dhcpd_conf_path(),
            "_PATH_": "/usr/sbin",
            "_LEASE_": self.get_lease_file_path(),
            "_LOCATION_": "/usr/sbin",
            "_DATA_": "/var/mmsuite/dhcp_server_controller",
            "_PARAM1_": "-u %s -g %s -c %s -e %s -d /var/mmsuite/dhcp_server_controller"
            % (user, group, self.get_dhcpd_conf_path(), self.get_lease_file_path()),
        }

    def prepare_data_directories(self):
        user, group = self.get_service_user_group()
        self.add_directory_info(
            "/var/mmsuite/dhcp_server_controller", user, group, "755"
        )
        self.add_directory_info(
            "/var/mmsuite/dhcp_server_controller/scripts", user, group, "755"
        )
        self.add_file_info(
            os.path.join(os.getcwd(), "scripts", "restartdhcp.sh"),
            "/var/mmsuite/dhcp_server_controller/scripts",
            user,
            group,
            "755",
        )

    def get_dhcpd_conf_path(self):
        """
        Get path to dhcpd.conf config path.
        :return: Path to dhcpd.conf file.
        """
        return get_alternative_files(
            [
                "/replicated/etc/dhcpd.conf",
                "/etc/dhcp/dhcpd.conf",
                "/etc/dhcpd.conf",
                "/etc/dhcp3/dhcpd.conf",
            ]
        )

    def get_lease_file_path(self):
        """
        Get path to dhcpd.leases file.
        :return: Path to dhcpd.leases file.
        """
        return get_alternative_files(
            [
                "/replicated/var/state/dhcp/dhcpd.leases",  # BDDS
                "/var/lib/dhcpd/dhcpd.leases",
                "/var/lib/dhcp3/dhcpd.leases",  # Ubuntu
                "/var/lib/dhcp/dhcpd.leases",  # Red Hat
                "/var/lib/dhcp/db/dhcpd.leases",
            ]  # OpenSUSE
        )

    def get_systemd_template_path(self):
        if is_ubuntu():
            return os.path.join(
                os.getcwd(), "startupscripts/ubuntu/mmdhcpremote.isc.service"
            )
        else:
            return os.path.join(os.getcwd(), "startupscripts/mmdhcpremote.isc.service")


class KeaRemoteInstaller(BaseDHCPRemoteInstaller):
    """
    Install class for the Micetro Kea DHCP server controller.
    """

    def __init__(self, settings={}):
        BaseDHCPRemoteInstaller.__init__(self, settings=settings)

    def arrange_config(self):
        # Write values to preferences
        user_provided_socket_name = self.get_settings().get(
            "kea-dhcp4-socket-name", None
        )
        if user_provided_socket_name is not None:
            self.add_preference_value(
                "/var/mmsuite/dhcp_server_controller/preferences.cfg",
                "keaControlChannelSocketName",
                user_provided_socket_name,
            )

        user_provided_socket_v6_name = self.get_settings().get(
            "kea-dhcp6-socket-name", None
        )
        if user_provided_socket_v6_name is not None:
            self.add_preference_value(
                "/var/mmsuite/dhcp_server_controller/preferences.cfg",
                "keaControlChannelSocketName6",
                user_provided_socket_v6_name,
            )


class DNSRemoteInstaller(Installer):
    """
    Base class for the Micetro DNS server controller installers.
    """

    def __init__(self, settings={}):
        Installer.__init__(self, "mmremote", "DNS Remote", 1337, settings=settings)

    def check_environment(self):
        if get_platform() == "sunos" and not os.path.exists("/etc/netconfig"):
            raise InstallError(
                "The DNS Server Controller will not run on Solaris without a netconfig file. Please make sure that a netconfig file exists and try again.\n\nInstallation aborted.\n"
            )

    def get_dns_server_controller_dir_path(self):
        """
        Get path to dns_server_controller dir.
        :return: Path to dns_server_controller dir.
        """
        return "/var/mmsuite/dns_server_controller"

    def prepare_data_directories(self):
        user, group = self.get_service_user_group()
        self.add_directory_info(
            self.get_dns_server_controller_dir_path(), user, group, "755"
        )


class BindDNSRemoteInstaller(DNSRemoteInstaller):
    """
    Install class for the Micetro Bind DNS server controller.
    """

    def __init__(self, chroot_dir=None, settings={}):
        assert chroot_dir is None or os.path.exists(chroot_dir)
        self.chroot_dir = chroot_dir

        self.named_conf_alternative_paths = [
            "/etc/named.conf",
            "/etc/bind/named.conf",  # Ubuntu
            "/etc/namedb/named.conf",
            "/etc/opt/isc/isc-bind/named.conf",
            "/etc/opt/isc/scls/isc-bind/named.conf",
        ]

        named_conf_path = settings.get("named-config-file-path", None)

        if named_conf_path:
            named_conf_path = os.path.abspath(named_conf_path)
            if not os.path.isfile(named_conf_path):
                raise InstallError('"%s" is not a valid file.' % str(named_conf_path))
            self.named_conf_alternative_paths = [named_conf_path]

        DNSRemoteInstaller.__init__(self, settings=settings)

    def check_environment(self):
        DNSRemoteInstaller.check_environment(self)

        # If its specified as an argument that the user wants to install a
        # Bind controller we skip checking if Bind is installed or running
        if not install_settings["bind-dns-controller"] and not has_bind():
            raise InstallError(
                "Unable to install BIND DNS server controller. No BIND installation detected."
            )

    def prepare_config(self):
        user, group = self.get_service_user_group()
        param1 = "-u %s -g %s -c %s" % (
            user,
            group,
            self.get_named_conf_path(include_chroot_path=False),
        )
        root_dir = ""
        root_dir_param = ""
        if self.chroot_dir is not None:
            param1 += " -t %s" % self.chroot_dir
            root_dir = self.chroot_dir
            root_dir_param = "-r" + root_dir

        self.service_data = {
            "_USER_": user,
            "_GROUP_": group,
            "_CONF_": self.get_named_conf_path(include_chroot_path=False),
            "_PATH_": "/usr/sbin",
            "_ROOTDIR_": root_dir,
            "_ROOTDIRPARAM_": root_dir_param,
            "_DATA_": "",
            "_LOCATION_": "/usr/sbin",
            "_PARAM1_": param1,
        }

    def get_service_user_group(self):
        # If user supplies user name and group, assume that BIND is running as that user/group.  Otherwise, fall back to named/named, bind/bind or root/root
        user = self.settings.get("bind-user")
        group = self.settings.get("bind-group")
        if user or group:
            if not (user and group):
                raise InstallError("Please specify both a user and group name for BIND")
            if not has_user(user):
                raise InstallError('User "' + user + '" does not exist')
            if not has_group(group):
                raise InstallError('Group "' + group + '" does not exist')
            return get_alternative_user_group([(user, group)])
        return get_alternative_user_group([("named", "named"), ("bind", "bind")])

    def get_named_conf_path(self, include_chroot_path=False):
        """
        Get path to named.conf.
        :param include_chroot_path: If True and Bind is running in a chroot, then the full path will be returned,
                                    If False, then a path relative to the chroot will be returned.
                                    Defaults to False.
                                    Example: self.get_named_conf_path(include_chroot_path=False) -> /etc/named.conf.
                                             self.get_named_conf_path(include_chroot_path=True) -> /chroot//etc/named.conf.

        :return: Path to named.conf.
        """

        if self.chroot_dir is None:
            return get_alternative_files(self.named_conf_alternative_paths)
        else:
            chroot_alternate_files = [
                self.get_path_with_chroot(alternative_file)
                for alternative_file in self.named_conf_alternative_paths
            ]
            named_conf_path = get_alternative_files(chroot_alternate_files)
            if include_chroot_path or named_conf_path is None:
                return named_conf_path
            else:
                # Strip out chroot part
                return os.path.join(
                    "/", named_conf_path.replace(str(self.chroot_dir), "")
                )

    def get_path_with_chroot(self, path):
        if self.chroot_dir is None:
            return path
        else:
            return os.path.join(self.chroot_dir, path.lstrip("/"))

    def get_arrange_cmd(self, added_params=[]):
        # Builds "arrange" cmd with the correct params to find named.conf either inside or outside a chroot
        # Caller can specify additional parameters depending on what arrange should be doing
        arrange_bin = os.path.join(os.getcwd(), self._get_bin_src_dir(), "arrange")
        arrange_cmd = [
            arrange_bin,
            "-c",
            self.get_named_conf_path(include_chroot_path=False),
        ]
        if self.chroot_dir:
            arrange_cmd += ["-t", self.chroot_dir]
        return arrange_cmd + added_params

    def get_data_dir(self):
        return run_command_or_exit(
            self.get_arrange_cmd(["-p"]),
            "An error came up while arranging the BIND configuration",
        ).strip()

    def arrange_config(self):
        start_bind = False
        user, group = self.get_service_user_group()

        if self.get_named_conf_path(include_chroot_path=True) is None:
            raise InstallError("Unable to find named.conf file.  Is BIND installed?")

        data_dir = self.get_data_dir()  # Get directory option from named.conf
        real_data_dir = self.get_path_with_chroot(data_dir)
        if not install_settings["skip-arrange"] and not is_named_conf_arranged(
            self.get_named_conf_path(include_chroot_path=True)
        ):
            self.service_control_bind("stop")
            start_bind = True
            if is_redHat():
                self.wait_for_bind_chroot_umount()
            self.arrange_bind_conf(data_dir)

            # We need real directory here
            self.chown(real_data_dir, user, group, recursive=True)

        # make sure that the parent directory of dns_server_controller is readable
        # else mmremote will not be able to read it's preferences.cfg file if it
        # is not running as root
        self.chmod(
            os.path.dirname(self.get_dns_server_controller_dir_path()), "755", True
        )

        # Create /var/mmsuite/dns_server_controller/preferences.cfg and write some initial data to it
        preferences_file = os.path.join(
            self.get_dns_server_controller_dir_path(), "preferences.cfg"
        )
        run_command_or_exit(
            ["touch", preferences_file], "Unable to create file " + preferences_file
        )
        self.chown(preferences_file, user, group)

        # Set ownership of configuration files to same user as is running Bind:
        for path in [real_data_dir, self.get_named_conf_path(include_chroot_path=True)]:
            self.chown(path, user, group, recursive=True)

        # Make sure that log file exists
        log_path = os.path.join(real_data_dir, "mmsuite.log")
        run_command_or_exit(["touch", log_path], "Unable to create file " + log_path)
        self.chown(log_path, user, group, recursive=False)

        if start_bind:
            self.service_control_bind("start")

    def arrange_bind_conf(self, data_dir):
        """
        Arrange Bind config.
        """
        self.print_msg(
            "Arranging Bind config in %s" % self.get_path_with_chroot(data_dir)
        )
        # Create tmp dir beside data dir that arrange uses while working:
        tmp_dir = data_dir + ".mm-arrange"

        # Run arrange
        out = run_command_or_exit(
            self.get_arrange_cmd(["-q", tmp_dir]),
            "An error came up while arranging the BIND configuration",
        )

        # Special cases for Red Hat/Centos.
        # If either the directory "data" or "chroot" exist inside the named.bak directory,
        # then we copy them to the newly arranged directory.
        if is_redHat():
            data_dir_bak = data_dir + ".bak"
            for dir in ["data", "chroot"]:
                src_dir = os.path.join(data_dir_bak, dir)
                dst_dir = os.path.join(data_dir, dir)
                if os.path.exists(src_dir) and not os.path.exists(dst_dir):
                    run_shell_command("cp -r %s %s" % (src_dir, dst_dir))

        # print output from arrange
        for line in str(out).split(os.linesep):
            self.print_msg(line)

    def wait_for_bind_chroot_umount(self, max_sleep_time=5):
        """
        Some installations (namely Red Hat/Centos) mount the named data directory when Bind is running in a chroot.
        The installer has to make sure that named-chroot has unmounted the named directory, before doing arrange on the config.

        :return: True if named directory is unmounted, else False.
        """
        elapsed_time = 0
        out = None
        while str(out).strip() != "" and elapsed_time < max_sleep_time:
            time.sleep(1)  # Sleep for one second before checking
            out, _ = run_shell_command("mount|grep named")
            elapsed_time += 1

        return str(out).strip() == ""

    def get_bind_service_name(self):
        """
        Get service name that bind runs as.
        :return: The name of the service.
        """
        service_name = "named"
        if is_ubuntu():
            service_name = "bind9"
        elif is_iscBuilt():
            service_name = "isc-bind-named"
        elif is_redHat() and is_systemd():
            # Special case for bind running in a chroot
            out, status = run_command(
                [get_systemctl_path(), "is-enabled", "named-chroot"]
            )
            if status == 0 and "enabled" in str(out).lower():
                service_name = "named-chroot"

        return service_name

    def service_control_bind(self, action):
        """
        Stop, start or restart Bind.
        """
        assert action in ["start", "stop", "restart"]

        if not is_bdds():
            service_name = self.get_bind_service_name()
            self.print_msg("Attempting to %s Bind." % action)
            if not service_control(service_name, action) and action in [
                "start",
                "restart",
            ]:
                print("Unable to %s Bind. Please %s Bind manually." % (action, action))


class GenericDNSRemoteInstaller(DNSRemoteInstaller):
    """
    Install class for the Micetro generic DNS server controller.
    """

    def prepare_config(self):
        user, group = self.get_service_user_group()
        self.service_data = {
            "_USER_": user,
            "_GROUP_": group,
            "_CONF_": "",
            "_DATA_": self.get_dns_server_controller_dir_path(),
            "_PATH_": "/usr/sbin",
            "_LOCATION_": "/usr/sbin",
            "_ROOTDIR_": "",
            "_PARAM1_": "-u %s -g %s -d /var/mmsuite/dns_server_controller"
            % (user, group),
        }

    def get_systemd_template_path(self):
        if is_ubuntu():
            return os.path.join(
                os.getcwd(), "startupscripts/ubuntu/mmremote.generic.service"
            )
        else:
            return os.path.join(os.getcwd(), "startupscripts/mmremote.generic.service")


def has_installer_content(content):
    """
    Check if "content" is in installer contents file.
    :param content: Content to check for.
    :return: True if content is found, else False.
    """
    f = None
    try:
        f = open(os.path.join(os.getcwd(), "contents.txt"))
        lines = f.readlines()
        f.close()
        for line in lines:
            if content in line:
                return True
        return False
    except:
        if f is not None:
            f.close()
        raise InstallError(
            "Unable to determine installer type. Please contact support@bluecatnetworks.com.\n"
        )


def is_central_installer():
    """
    Check if this is a Central installer.
    :return: True if this is a Central installer, else False.
    """

    return has_installer_content("mmcentrald")


def has_mmxmlint():
    """
    Tries to detect the old web stuff so we can warn the user and have them remove it
    before preceding. Note that this only searches the default install locations
    """
    data_directory = get_alternative_files(["/var/mmsuite/xml_interface"])
    binary_path = get_alternative_files(["/usr/sbin/mmxmlintd"])
    if data_directory and binary_path:
        return os.path.isdir(data_directory) and os.path.isfile(binary_path)
    return False


def is_server_controller_installer():
    """
    Check if this is a server controller installer.
    :return: True if this is a server controller installer, else False.
    """
    return has_installer_content("mmremoted")


def is_web_installer():
    """
    Check if this is a web service/web installer.
    :return: True if this is a web installer, else False.
    """
    return has_installer_content("mmwsd")

def is_hub_head_installer():
    """
    Check if this is an installer for the Cloud managed DDI remotes.
    :return: True if this is a CMDDI remote installer, else False.
    """
    return is_server_controller_installer() and has_installer_content("hub-head.yaml")

def has_web_app_configuration():
    """
    Check if a previous install left us a file describing previous installation of the web app.
    :return: True if found, else false
    """
    return os.path.isfile("/var/mmsuite/web_services/web_app_configuration")


def arg_parse(arg_settings, user_args):
    """
    Parse arguments from "user_args" and return a dictionary containing True/False values, depending on user options.
    :param arg_settings: Configuration for valid and default values.
    :param user_args: Arguments provided by user.
    :return: A dictionary where keys are arguments and values are True if selected by a user and False otherwise.
    :raises InstallError if user_args contains an invalid argument.
    """
    ARG_NAME = 0
    ARG_DESCRIPTION = 1
    ARG_TYPE = 2
    ARG_DEFAULT = 3

    legal_args = ["--%s" % arg[ARG_NAME] for arg in arg_settings]
    settings = {}
    settings_info = {}

    for arg_setting in arg_settings:
        assert len(arg_setting) == 4
        settings[arg_setting[ARG_NAME]] = arg_setting[ARG_DEFAULT]
        settings_info[arg_setting[ARG_NAME]] = arg_setting

    arg_num = 0
    while arg_num < len(user_args):
        user_arg = user_args[arg_num]
        if user_arg not in legal_args:
            raise InstallError("Unknown argument %s" % user_arg)
        else:
            # Get argument setting info
            arg_setting_info = settings_info[
                user_arg[2:]
            ]  # clip "--" from the front of "--<arg-name>"
            if arg_setting_info[ARG_TYPE] == bool:
                settings[arg_setting_info[ARG_NAME]] = True
            else:
                assert arg_setting_info[ARG_TYPE] == str
                # Check if this is the last element
                if arg_num + 1 == len(user_args):
                    raise InstallError('No value given for "%s" option' % user_arg)
                arg_value = str(user_args[arg_num + 1]).strip()
                # Got something like "--the-installer-argument <arg_value>". Now we need to jump over the value to get a
                # valid argument in the next iteration.
                arg_num += 1
                if (
                    arg_value in legal_args
                ):  # The value is an argument (e.g. "--bind-dns-controller") for the installer
                    raise InstallError('No value given for "%s" option' % user_arg)
                elif arg_value.startswith("--"):
                    raise InstallError(
                        'The value "%s" given for "%s" is not a valid value.'
                        % (arg_value, user_arg)
                    )
                else:
                    settings[arg_setting_info[ARG_NAME]] = arg_value
        arg_num += 1

    return settings


def print_help(arg_settings):
    """
    Print help menu.

    :param arg_settings: A List of (<argument name>, <argument help>, <argument type>, <default value>) tuples.
    """
    installer_for_text = "server controller"
    if is_central_installer():
        installer_for_text = "Central"
    elif is_web_installer():
        installer_for_text = "Web services"

    print("Micetro %s installer." % installer_for_text)
    for arg, param_doc, _, _ in arg_settings:
        print("--%s:  %s" % (arg, param_doc))


if __name__ == "__main__":

    try:
        args = sys.argv[1:]  # Strip out program name
        arg_settings = [
            ("help", "Print help.", bool, False),
            ("quiet", "Suppress output during install.", bool, False),
        ]
        if is_server_controller_installer():
            arg_settings += [
                (
                    "auto",
                    "Automatically determine what controllers to install. Default if no other option is given.",
                    bool,
                    False,
                ),
                (
                    "bind-dns-controller",
                    "Install a DNS server controller for BIND.",
                    bool,
                    False,
                ),
                ("bind-user", "User account that BIND runs under.", str, None),
                ("bind-group", "Group that BIND user is member of.", str, None),
                (
                    "named-checkconf-path",
                    "Path to named-checkconf. Only applies to a DNS server controller for BIND.",
                    str,
                    None,
                ),
                (
                    "named-config-file-path",
                    "Path to named.conf. Only applies to a DNS server controller for BIND.",
                    str,
                    None,
                ),
                (
                    "named-directory-path",
                    "Directory for BIND configuration files. Only applies to a DNS server controller for BIND.",
                    str,
                    None,
                ),
                (
                    "skip-arrange",
                    "Will not arrange the configuration files for BIND.",
                    bool,
                    False,
                ),
                (
                    "generic-dns-controller",
                    "Install a Generic DNS server controller.",
                    bool,
                    False,
                ),
                (
                    "isc-dhcp-controller",
                    "Install a DHCP server controller for ISC dhcpd.",
                    bool,
                    False,
                ),
                (
                    "kea-dhcp-controller",
                    "Install a DHCP server controller for Kea-dhcp.",
                    bool,
                    False,
                ),
                (
                    "kea-dhcp4-socket-name",
                    "Socket name for kea-dhcp4 control channel.",
                    str,
                    None,
                ),
                (
                    "kea-dhcp6-socket-name",
                    "Socket name for kea-dhcp6 control channel.",
                    str,
                    None,
                ),
                (
                    "standalone-dhcp-controller",
                    "Install a Standalone DHCP server controller.",
                    bool,
                    False,
                ),
            ]
        if is_hub_head_installer():
            arg_settings += [
                (
                    "token",
                    "Token for connecting to the cloud.",
                    str,
                    None,
                ),
            ]
        if is_web_installer():
            arg_settings += [
                (
                    "auto",
                    "Installs the Micetro web services and the web applications. Default if no other option is given.",
                    bool,
                    False,
                ),
                ("web-service", "Install a web service component. ", bool, False),
                (
                    "web-application",
                    "Install web application and relative Apache config. Note: Requires the web service component to connect to the central component.",
                    bool,
                    False,
                ),
                (
                    "web-virtual-host-domain",
                    "Specify a DNS name to use for the virtual host configuration when installing for the first time (or to change the address the web application answers on).",
                    str,
                    None,
                ),
                (
                    "force",
                    "Force new virtual host domain and modify the apache2 configuration by hand",
                    bool,
                    False,
                )
            ]

        arg_settings += [
            (
                "update-controller",
                "Install update controller. Always installed, if another Micetro service is installed.",
                bool,
                False,
            ),
        ]

        install_settings = {}
        install_settings = arg_parse(arg_settings, args)
        # Disabled until Bind overlay is fully supported

        # Check if selection is valid:
        if is_server_controller_installer():
            has_dns_remote_selected = False
            for key in ["bind-dns-controller", "generic-dns-controller"]:
                if has_dns_remote_selected and install_settings[key]:
                    raise InstallError(
                        "Unable to install more than one type of DNS server controller."
                    )
                has_dns_remote_selected = (
                    has_dns_remote_selected or install_settings[key]
                )

            if (
                install_settings["isc-dhcp-controller"]
                and install_settings["kea-dhcp-controller"]
                and install_settings["standalone-dhcp-controller"]
            ):
                raise InstallError(
                    "Unable to install more than one type of DHCP server controller."
                )

        if is_hub_head_installer():
            # If we are upgrading cmddi controllers, check if the current token exists and if it is the same as the newly supplied token
            existing_token = check_cmddi_token_existence()
            if install_settings["token"] and existing_token is not None and install_settings["token"] != existing_token:
                raise InstallError(
                    f"""
Existing token: {existing_token}
is not the same as the one supplied as an argument: {install_settings['token']}

If you need to use a new token, update the `token` field manually in /var/mmsuite/hub-head/hub-head.yaml.

Since changing the token will require changing the keys, manually removing the /var/mmsuite/hub-head/bundle.pem file is also needed.
"""
                )

            # To silence further checks, just set install_settings["token"] to the previously installed value
            if not install_settings["token"] and existing_token is not None:
                install_settings["token"] = existing_token

            # Check if token is supplied when CMDDI remotes are being installed
            if not install_settings["token"]:
                raise InstallError(
                    "You must supply the token to install and setup CMDDI agents"
                )

    except InstallError as e:
        print(str(e))
        try:
            print_help(arg_settings)
        except:
            pass
        exit(1)

    if install_settings.get("help"):
        print_help(arg_settings)
        exit(0)

    try:
        is_verbose = not install_settings.get("quiet", False)
        do_pre_install_check()
        installers = []
        if is_central_installer():
            installers.append(CentralInstaller(settings=install_settings))
        elif is_web_installer():
            if has_mmxmlint():
                print(
                    "Detected a deprecated version of the Micetro web service (mmxmlintd) on the machine."
                )
                print("Please remove it before installing the new Web service.")
                exit(1)
            if has_apache():
                print("Apache web service detected.")
                # we check if its running, since we rely on stuff to have been created already
                if not is_apache_running():
                    print(
                        "Please ensure Apache service is running and its default configuration and log files have already been created."
                    )
                    exit(1)
                # neither was specified or auto was true
                both_implied = install_settings["auto"] or (
                    not install_settings["web-application"]
                    and not install_settings["web-service"]
                )
                is_installing_web = install_settings["web-application"] or both_implied

                if (
                    is_installing_web
                    and not install_settings["web-virtual-host-domain"]
                ):
                    if not has_web_app_configuration():
                        print(
                            "Please specify a virtual domain host name to use for the web application, see '--web-virtual-host-domain' parameter."
                        )
                        exit(1)
                # web service was specified or both implied
                if install_settings["web-service"] or both_implied:
                    install_settings["web-service"] = True
                    installers.append(WebServiceInstaller(settings=install_settings))
                # web app was specified or both implied
                if install_settings["web-application"] or both_implied:
                    install_settings["web-application"] = True
                    installers.append(WebInterfaceInstaller(settings=install_settings))
            else:
                print(
                    "Apache service was not detected. Apache is a prerequisite for installing the Micetro web services."
                )
        elif is_server_controller_installer():
            # Server controllers
            # Check if we should auto detect what to install

            if is_bdds():
                # If this is the bdds installer, we want to install BIND, ISC DHCP and update agent
                install_settings["bind-dns-controller"] = True
                install_settings["isc-dhcp-controller"] = True
                install_settings["update-controller"] = True
                install_settings["auto"] = False
                run_bdds_setup_script("install")

            if (
                not install_settings["bind-dns-controller"]
                and not install_settings["generic-dns-controller"]
                and not install_settings["update-controller"]
                and not install_settings["isc-dhcp-controller"]
                and not install_settings["kea-dhcp-controller"]
                and not install_settings["standalone-dhcp-controller"]
            ) or install_settings["auto"]:

                # DNS server controller
                if has_bind():
                    if is_verbose:
                        print("Bind DNS server detected.")
                    installers.append(
                        BindDNSRemoteInstaller(
                            get_bind_chroot(), settings=install_settings
                        )
                    )

                # DHCP server controller
                if has_isc_dhcp():
                    if is_verbose:
                        print("ISC DHCP server detected.")
                    installers.append(ISCRemoteInstaller(settings=install_settings))

                # Kea server controller
                if has_kea_dhcpv4():
                    if is_verbose:
                        print("Kea DHCP server detected.")
                    installers.append(KeaRemoteInstaller(settings=install_settings))

                if len(installers) > 1:
                    raise InstallError(
                        "More than one server controller type installation is possible on this machine. Please specify the server controller type you want to install."
                    )
                elif (
                    len(installers) == 1
                    and installers[0].service_name != "mmremote"
                    and install_settings["skip-arrange"]
                ):
                    raise InstallError(
                        "The skip-arrange argument can only be used when installing a BIND DNS server controller"
                    )
            else:
                # Determine what to install from arguments and check skip-arrange requirements
                if (
                    install_settings["skip-arrange"]
                    and not install_settings["bind-dns-controller"]
                ):
                    raise InstallError(
                        "The skip-arrange argument can only be used when installing a BIND DNS server controller"
                    )

                if install_settings["bind-dns-controller"]:
                    installers.append(
                        BindDNSRemoteInstaller(
                            get_bind_chroot(), settings=install_settings
                        )
                    )
                elif install_settings["generic-dns-controller"]:
                    installers.append(
                        GenericDNSRemoteInstaller(settings=install_settings)
                    )

                if install_settings["isc-dhcp-controller"]:
                    installers.append(ISCRemoteInstaller(settings=install_settings))
                elif install_settings["kea-dhcp-controller"]:
                    installers.append(KeaRemoteInstaller(settings=install_settings))
                elif install_settings["standalone-dhcp-controller"]:
                    installers.append(BaseDHCPRemoteInstaller(settings=install_settings))

        if is_hub_head_installer():
            installers.append(HubHeadInstaller(settings=install_settings))

        if installers or install_settings["update-controller"]:
            installers.append(UpdaterInstaller(settings=install_settings))
        else:
            # Nothing to do
            print("No server controller type detected for installation.")

        # Install
        for installer in installers:
            installer.set_verbose(is_verbose)
            installer.do_install()

        if is_bdds():
            run_bdds_setup_script("post_install")

        if installers and is_verbose:
            print("%sInstallation complete." % os.linesep)

            if is_web_installer():
                installers[0].apache_restart_message()
                print("")
            ports = [installer.get_port() for installer in installers]
            if len(ports) == 1:
                print(
                    "Please make sure that port %i is open on this machine." % ports[0]
                )
            elif len(ports) > 1:
                print(
                    "Please make sure that ports %s and %i are open on this machine."
                    % (str(ports[:-1]).replace("[", "").replace("]", ""), ports[-1])
                )

    except InstallError as e:
        print(str(e))
        exit(1)

    exit(0)
