#!/usr/bin/python3
# Copyright (C) Eenos.com
# Eenos High Performance Hosting Control Panel
#
# This file is part of Eenos.
#
# Licensed under the Eenos Public License (EPL).
# License: https://eenos.com/epl/
#
# Author: Eenos Development Team
# Website: https://eenos.com

import os
import sys

try:
    import configparser
    import datetime
    import logging
    import pwd
    import subprocess
    import tempfile
    from logging.handlers import RotatingFileHandler
    from pprint import pprint

    import yaml
except Exception as e:
    print(str(e))
    print("Error loading python module in system python")
    print("Auto Update Failed")
    sys.exit(0)


# Just some shell colors
class scolor:
    pink = "\033[95m"
    blue = "\033[94m"
    green = "\033[92m"
    warning = "\033[93m"
    fail = "\033[91m"
    end = "\033[0m"
    bold = "\033[1m"
    underline = "\033[4m"


formatter = logging.Formatter(
    "[%(asctime)s +%(msecs)04d] - %(name)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
cformatter = logging.Formatter(
    "[%(asctime)s +%(msecs)04d] - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
uformatter = logging.Formatter(
    "[%(asctime)s +%(msecs)04d] - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)


def get_console_handler():
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setFormatter(cformatter)
    return console_handler


def get_file_handler(logfile):
    uid = pwd.getpwnam("eenos").pw_uid
    logpath = "/var/log/eenos/" + logfile
    file_handler = RotatingFileHandler(logpath, maxBytes=0, backupCount=10)
    file_handler.doRollover()
    file_handler.setFormatter(formatter)
    if os.path.isfile(logpath):
        os.chown(logpath, uid, uid)
    return file_handler


def get_logger(logfile):
    if not os.path.exists("/var/log/eenos"):
        os.makedirs("/var/log/eenos")
        os.chmod("/var/log/eenos", 0o755)
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    if sys.stdin.isatty():
        logger.addHandler(get_console_handler())
    logger.addHandler(get_file_handler(logfile))
    logger.propagate = False
    return logger


def readyaml(datafile, log):
    if os.path.isfile(datafile):
        with open(datafile, "r") as stream:
            try:
                data_loaded = yaml.load(stream, Loader=yaml.FullLoader)
            except ValueError as e:
                log(e)
                data_loaded = {}
    else:
        log("Datafile not found : " + datafile)
        data_loaded = {}
    return data_loaded


def readplain(dfile):
    if os.path.isfile(dfile):
        data = list()
        try:
            pre = []
            with open(dfile, "r") as f:
                lines = f.readlines()
            for l in lines:
                d = l.strip()
                pre.append(d)
            data = list(dict.fromkeys(pre))
        except Exception as e:
            data = []
    else:
        data = list()
    return data


# Just for fun
def banner():
    now = datetime.datetime.now()
    sys.stdout.write("Copyright (C) 2023-" + str(now.year) + ", Eenos.com \n")
    sys.stdout.write("Eenos High Performance Hosting Control Panel\n")
    sys.stdout.write("Eenos auto update tool\n")
    sys.stdout.write(scolor.green)
    sys.stdout.write("""
 _  _       _   __ 
|_ |_ |\ | / \ (_  
|_ |_ | \| \_/ __) 
""")
    sys.stdout.write(scolor.end + "\n")

# The backup configuration file
backup_conf = "/var/eenos/preferences/autoupdate.yaml"

def autoupdate_enabled(log):
    if not os.path.isfile(backup_conf):
        return False

    if os.stat(backup_conf).st_size == 0:
        return False

    settings = readyaml(backup_conf, log)

    if not settings:
        return False

    return settings.get("status") == "on"


class commands:
    def exec(cmd, log):
        process = subprocess.Popen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
        )
        try:
            for line in iter(process.stdout.readline, ""):
                line = line.rstrip()

                if line:
                    log(line)
        finally:
            process.stdout.close()
        return process.wait()
    
    def exec_old(cmd, log):
        result = subprocess.run(
            cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
        )
        for line in result.stdout.splitlines():
            log(line)
        return result.returncode

    def exec_return(cmd, log=None):
        result = subprocess.run(
            cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
        )

        output = result.stdout.splitlines()

        if log:
            for line in output:
                log(line)

        return result.returncode, output

    def runandreturn(cmd):
        try:
            op = subprocess.Popen(
                [cmd],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=True,
                text=True,
            )
            res = op.communicate()
            res = list(filter(None, res))
            final = list()
            for i in res:
                x = i.split("\n")
                for q in x:
                    final.append(q)
            final = list(filter(None, final))
        except:
            final = list()
        return final

    def live(cmd, log):
        process = subprocess.Popen(
            [cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True
        )
        while True:
            op = process.stdout.readline().strip()
            if process.poll() is not None:
                break
            if op:
                log(op)
        rc = process.poll()
        return rc


def read_confgdata(configfile, log=None):
    if not os.path.isfile(configfile):
        return {}

    try:
        parser = configparser.ConfigParser()

        with open(configfile, "r") as stream:
            parser.read_string("[top]\n" + stream.read())

        return dict(parser["top"])

    except Exception as e:
        if log:
            log("Unable to read config file %s: %s" % (configfile, e))

        return {}


def get_os(log=None):
    os_release = "/etc/os-release"

    configs = read_confgdata(os_release, log)

    if not configs:
        return {}

    def value(key):
        return str(configs.get(key.lower(), "")).strip().strip('"')

    pretty_name = value("PRETTY_NAME")

    if pretty_name.startswith("Debian"):
        return {
            "os": "Debian",
            "name": value("VERSION_CODENAME"),
            "pretty_name": pretty_name,
        }

    if pretty_name.startswith("Ubuntu"):
        return {
            "os": "Ubuntu",
            "name": value("UBUNTU_CODENAME"),
            "pretty_name": pretty_name,
        }

    if pretty_name.startswith(("Red", "Rocky", "AlmaLinux", "CloudLinux")):
        return {
            "os": "RHEL",
            "name": value("ID"),
            "pretty_name": pretty_name,
        }

    return {}


def ubuntu_os_package_update(log):
    cmd = "/usr/bin/apt-get -y update"
    log(cmd)
    commands.exec(cmd,log)    
    log("---- The following OS components are available for update -------- ")
    cmd2 = "/usr/bin/apt-get -s upgrade"
    log(cmd2)
    commands.exec(cmd2,log)    
    log("------end of package list -------")
    log("Running upgrade ... ")
    cmd3 = "/usr/bin/apt-get -y upgrade"
    commands.exec(cmd3,log)    
    if os.path.isfile("/etc/apt/apt.conf.d/99needrestart"):
        try:
            os.unlink("/etc/apt/apt.conf.d/99needrestart")
        except Exception as e:
            log(str(e))
            pass

def rhel_os_package_update(log):
    log("Updating Operating System components")
    cmd = "/usr/bin/dnf -y update"
    log(cmd)
    commands.exec(cmd,log)

def license_update(log):
    log("Updating license for Eenos")
    lcmd = "/usr/local/eenos/bin/eenoslicense -u"
    commands.live(lcmd, log)
    lcmd = "/usr/local/eenos/bin/eenoslicense -c"
    commands.live(lcmd, log)


def secure_web(log):
    if os.path.isfile("/usr/local/eenos/bin/secure-eenos-web.sh"):
        log("Securing Eenos web forcefully ...")
        cmd = "/usr/local/eenos/bin/secure-eenos-web.sh"        
        commands.exec(cmd,log)
        # commands.live(cmd,log)


def ubuntu_eenos_web_update(this_tire, target_tire, available_tires, log):
    if target_tire not in available_tires:
        log("The selected target tire is not available: %s" % target_tire)
        return
    current_package = f"eenos-web-{this_tire}"
    target_package = f"eenos-web-{target_tire}"
    
    if this_tire == target_tire:
        log(
            "Updating Eenos web package from [%s] to [%s] - [%s]"
            % (this_tire, target_tire, current_package)
        )
        cmd = "/usr/bin/apt-get -y install %s" % current_package
        log(cmd)
        commands.exec(cmd, log)
    else:
        log(
            "Updating Eenos web package from [%s] to [%s] - [%s]"
            % (
                this_tire,
                target_tire,
                target_package,
            )
        )
        remove_cmd = "/usr/bin/apt-get -y --purge remove %s" % current_package
        log(remove_cmd)
        commands.exec(remove_cmd, log)
        install_cmd = "/usr/bin/apt-get -y install %s" % target_package
        log(install_cmd)
        commands.exec(install_cmd, log)
    secure_web(log)

def rhel_eenos_web_update(this_tire, target_tire, available_tires, log):
    if target_tire not in available_tires:
        log(f"The selected target tire is not available: {target_tire}")
        return

    current_package = f"eenos-web-{this_tire}"
    target_package = f"eenos-web-{target_tire}"

    if this_tire == target_tire:
        log(
            f"Updating Eenos web package from "
            f"[{this_tire}] to [{target_tire}] - "
            f"[{current_package}]"
        )

        cmd = f"/usr/bin/dnf -y install {current_package}"
        log(cmd)
        commands.exec(cmd, log)

    else:
        log(
            f"Updating Eenos web package from "
            f"[{this_tire}] to [{target_tire}] - "
            f"[{target_package}]"
        )

        remove_cmd = f"/usr/bin/dnf -y remove {current_package}"
        log(remove_cmd)
        commands.exec(remove_cmd, log)

        install_cmd = f"/usr/bin/dnf -y install {target_package}"
        log(install_cmd)
        commands.exec(install_cmd, log)

    secure_web(log)

def eenos_pip_update(this_tire, target_tire, available_tires, log):
    old_tmpdir = os.environ.get("TMPDIR")
    tmpdir = "/root/tmp"
    os.makedirs(tmpdir, exist_ok=True)
    os.environ["TMPDIR"] = tmpdir
    try:
        pip = "/usr/local/eenos/bin/pip3"
        pip_options = (
            f"{pip} --no-color --no-cache-dir "
            f"install --upgrade"
        )
        # Update pip, setuptools and wheel.
        log("Updating pip module")
        pip_cmd = (
            f"{pip_options} "
            f"pip setuptools wheel --root-user-action=ignore"
        )
        log(pip_cmd)
        commands.exec(pip_cmd, log)

        # Determine Eenos package repository.
        if target_tire in available_tires:
            index_url = f"https://pip.eenos.com/{target_tire}/"
        else:
            index_url = "https://pip.eenos.com/release/"

        # Update Eenos core package.
        eenos_cmd = (
            f"{pip_options} "
            f"--extra-index-url {index_url} "
            f"eenos --root-user-action=ignore"
        )

        log(
            f"Installing Eenos core package "
            f"from [{this_tire}] to [{target_tire}].."
        )
        log(eenos_cmd)
        commands.exec(eenos_cmd, log)

        # Update Eenos Certbot DNS plugin.
        certbot_cmd = (
            f"{pip_options} "
            f"--extra-index-url "
            f"https://pip.eenos.com/certbot-dns-eenos/ "
            f"certbot-dns-eenos "
            f"--root-user-action=ignore"
        )

        log("Updating certbot-dns-eenos")
        log(certbot_cmd)
        commands.exec(certbot_cmd, log)

    finally:
        # Restore the previous TMPDIR.
        if old_tmpdir is None:
            os.environ.pop("TMPDIR", None)
        else:
            os.environ["TMPDIR"] = old_tmpdir

def ubuntu_eenos_web_update_depreciated_remove(this_tire, target_tire, available_tires, log):
    cpkg = "eenos-web-" + this_tire
    tpkg = "eenos-web-" + target_tire
    if target_tire in available_tires:
        if cpkg == tpkg:
            cmd = "/usr/bin/apt-get -y install " + cpkg
            log(
                "Updating eenos web package  from [%s] to [%s] - [%s]"
                % (this_tire, target_tire, cpkg)
            )
            log(cmd)
            # Remove comment after testing
            commands.exec(cmd,log)
            # commands.live(cmd,log)
        else:
            log(
                "Updating Eenos web package  from [%s] to [%s] - [%s]"
                % (this_tire, target_tire, tpkg)
            )
            rcmd = "/usr/bin/apt-get -y --purge remove " + cpkg
            log(rcmd)
            # Remove comment after testing
            commands.exec(rcmd,log)
            # commands.live(rcmd,log)
            icmd = "/usr/bin/apt-get -y install " + tpkg
            log(icmd)
            # Remove comment after testing
            commands.exec(icmd,log)
            # commands.live(icmd,log)
        secure_web(log)
    else:
        log("The selected target is not available")

def rhel_eenos_web_update_depreciated_remote(this_tire, target_tire, available_tires, log):
    cpkg = "eenos-web-" + this_tire
    tpkg = "eenos-web-" + target_tire
    if target_tire in available_tires:
        if cpkg == tpkg:
            cmd = "/usr/bin/dnf -y install " + cpkg
            log(
                "Updating Eenos web package  from [%s] to [%s] - [%s]"
                % (this_tire, target_tire, cpkg)
            )
            log(cmd)
            # Remove comment after testing
            commands.exec(cmd,log)
            # commands.live(cmd,log)
        else:
            log(
                "Updating Eenos web package  from [%s] to [%s] - [%s]"
                % (this_tire, target_tire, tpkg)
            )
            rcmd = "/usr/bin/dnf -y remove " + cpkg
            log(rcmd)
            # Remove comment after testing
            commands.exec(rcmd,log)
            # commands.live(rcmd,log)
            icmd = "/usr/bin/dnf -y install " + tpkg
            log(icmd)
            # Remove comment after testing
            commands.exec(icmd,log)
            # commands.live(icmd,log)
        secure_web(log)
    else:
        log("The selected target is not available")

def eenos_pip_update_depreciated_remove(this_tire, target_tire, available_tires, log):
    ctmp = str(tempfile.gettempdir())
    tmdir = "/root/tmp"
    if not os.path.isdir("/root/tmp"):
        try:
            os.makedirs("/root/tmp")
        except Exception as e:
            log(str(e))
            pass
    os.environ["TMPDIR"] = tmdir
    cmd1 = "/usr/local/eenos/bin/pip3 --no-color --no-cache-dir install --upgrade pip setuptools wheel "
    log("Updating pip module")
    log(cmd1)
    # Remove comment after testing
    commands.exec(cmd1,log)
    # commands.live(cmd1,log)
    if target_tire in available_tires:
        cmd3 = (
            "/usr/local/eenos/bin/pip3 --no-color --no-cache-dir install --upgrade --extra-index-url https://pip.eenos.com/"
            + target_tire
            + "/ eenos --root-user-action=ignore"
        )
    else:
        cmd3 = "/usr/local/eenos/bin/pip3 --no-color --no-cache-dir install --upgrade --extra-index-url https://pip.eenos.com/release/ eenos --root-user-action=ignore"
    log("Installing Eenos core pips  from [%s] to [%s].." % (this_tire, target_tire))
    log(cmd3)
    cetbot = "/usr/local/eenos/bin/pip3 --no-color --no-cache-dir install --upgrade --extra-index-url https://pip.eenos.com/certbot-dns-eenos/  certbot-dns-eenos --root-user-action=ignore"
    # Remove comment after testing
    commands.exec(cmd3,log)
    commands.exec(cetbot,log)
    # commands.live(cmd3,log)
    os.environ["TMPDIR"] = ctmp

def ubuntu_eenoscore_update(log):
    log("Updating Eenos core packgaes")
    cmd = "/usr/bin/apt-get -y install eenoscore"
    # Remove comment after testing
    commands.exec(cmd,log)
    # commands.live(cmd,log)


def rhel_eenoscore_update(log):
    log("Updating Eenos core packgaes")
    cmd = "/usr/bin/dnf -y install eenoscore"   
    commands.exec(cmd,log)

def post_update(from_version, to_version):
    if os.path.isfile("/usr/local/eenos/bin/eenos_post_update"):
        log("Performing Post Updates")
        cmd = (
            "/usr/local/eenos/bin/eenos_post_update --from="
            + from_version
            + " --to="
            + to_version
        )
        commands.exec(cmd,log)
        
def stop_monitoring(log):
    log("Stopping monitoring service ...")
    cmd="systemctl stop  eenosmonitor.service"
    commands.exec(cmd,log)


def restart_services(log):
    log("Restarting Eenos services")

    services = {
        "eenosweb.service": "Eenos UI",
        "eenostaskd.service": "Eenos Task Manager",
        "eenosbfd.service": "Eenos BFD",
        "edoveauthd.service": "Eenos Dovecot",
        "efiled.service": "Eenos Workspace",
    }

    for service, name in services.items():
        log("Restarting service: %s (%s)" % (name, service))  # noqa: UP031
        cmd = "systemctl restart %s" % service
        commands.exec_return(cmd, log)
    log("Eenos service restart completed")

def start_monitoring(log):
    log("Starting monitoring service ...")
    cmd="systemctl start eenosmonitor.service"
    commands.exec(cmd,log)


def report(old_version, old_tire, log):
    current = commands.runandreturn("/usr/local/eenos/scripts/eenosversion")
    new_version = current[0].split(":")[1]
    new_tire = current[1].split(":")[1]
    post_update(old_version, new_version)
    log(
        "Eenos update completed from [ %s (%s) ] to [ %s (%s) ]"
        % (old_version, old_tire, new_version, new_tire)
    )
    log("Done.")


def ensure_efield(log):
    # Remove this from future versions 4
    if os.path.isfile("/usr/bin/apt-get"):
        cmd = "/usr/bin/apt-get -y install eenos-efiled"
    else:
        cmd = "/usr/bin/dnf -y install eenos-efiled"
    if not os.path.isfile("/usr/local/eenos/sbin/efiled"):
        commands.exec(cmd, log)


def ubuntu_update(this_tire, target_tire, available_tires, settings, thisversion, log):
    stop_monitoring(log)
    if "osupdate" in settings and settings["osupdate"] == "on":
        log("Starting Operating Systems Package Update ..")
        ubuntu_os_package_update(log)
    ensure_efield(log)
    eenos_pip_update(this_tire, target_tire, available_tires, log)
    ubuntu_eenos_web_update(this_tire, target_tire, available_tires, log)
    ubuntu_eenoscore_update(log)
    restart_services(log)
    start_monitoring(log)
    license_update(log)
    report(thisversion, this_tire, log)


def rhel_update(this_tire, target_tire, available_tires, settings, thisversion, log):
    stop_monitoring(log)
    if "osupdate" in settings and settings["osupdate"] == "on":
        log("Starting Operating Systems Package Update ..")
        rhel_os_package_update(log)
    ensure_efield(log)
    eenos_pip_update(this_tire, target_tire, available_tires, log)
    rhel_eenos_web_update(this_tire, target_tire, available_tires, log)
    rhel_eenoscore_update(log)
    restart_services(log)
    start_monitoring(log)
    license_update(log)
    report(thisversion, this_tire, log)


def start(log):
    banner()
    log("Starting Eenos auto update ..")
    if not autoupdate_enabled(log):
        log("Eenos auto update not enabled")
        return

    settings = readyaml(backup_conf, log)
    if not settings or "tire" not in settings:
        log("Invalid auto update configuration")
        return
    current = commands.runandreturn("/usr/local/eenos/scripts/eenosversion")
    if not current or len(current) < 3:
        log("Unable to determine Eenos version information")
        return
    try:
        thisversion = current[0].split(":", 1)[1].strip()
        this_tire = current[1].split(":", 1)[1].strip()
        available_tires = [
            tire.strip()
            for tire in current[2].split(":", 1)[1].split(",")
            if tire.strip()
        ]
    except (IndexError, AttributeError):
        log("Invalid Eenos version information")
        return
    target_tire = str(settings["tire"]).strip()
    log("Detecting Eenos installed version : %s" % thisversion)
    log("Current installed TIRE : %s" % this_tire)
    log("Target update TIRE : %s" % target_tire)

    if target_tire not in available_tires:
        log("There is no update available to the selected tire yet. Update aborted !!")
        return

    if not os.path.isfile("/etc/os-release"):
        log("Unable to detect operating system")
        return

    osdata = get_os(log)
    if not osdata or "os" not in osdata:
        log("Unable to detect operating system")
        return

    os_name = osdata["os"]
    pretty_name = osdata.get("pretty_name", os_name)

    if os_name in ("Ubuntu", "Debian"):
        log("Eenos Updating for : %s" % pretty_name)
        os.environ["DEBIAN_FRONTEND"] = "noninteractive"        
        ubuntu_update(
            this_tire,
            target_tire,
            available_tires,
            settings,
            thisversion,
            log,
        )

    elif os_name == "RHEL":
        log("Eenos Updating for : %s" % pretty_name)
        rhel_update(
            this_tire,
            target_tire,
            available_tires,
            settings,
            thisversion,
            log,
        )
    else:
        log("Unsupported operating system : %s" % pretty_name)


if __name__ == "__main__":

    if os.geteuid() != 0:
        print("You don't have privilege to run this tool")
        sys.exit(2)
    logfile = "eenos-update.log"
    logger = get_logger(logfile)
    log = logger.info
    try:
        devserver_lock = "/etc/eenos_devserver"
        if os.path.isfile(devserver_lock):
            print(
                "Eenos dev server. Ensure backup and remove the "
                "lock '/etc/eenos_devserver' to run update"
            )
        else:
            start(log)
    finally:
        for handler in logger.handlers[:]:
            handler.close()
            logger.removeHandler(handler)
    sys.exit(0)