#   BAREOS - Backup Archiving REcovery Open Sourced
#
#   Copyright (C) 2013-2019 Bareos GmbH & Co. KG
#
#   This program is Free Software; you can redistribute it and/or
#   modify it under the terms of version three of the GNU Affero General Public
#   License as published by the Free Software Foundation and included
#   in the file LICENSE.
#
#   This program is distributed in the hope that it will be useful, but
#   WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
#   Affero General Public License for more details.
#
#   You should have received a copy of the GNU Affero General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
#   02110-1301, USA.

# -*- coding: utf-8 -*-
#
"""Bareos Client Configuration Listener Module."""

__package__ = ""  # workaround for PEP 366
import grp
import os
import stat
import string
from subprocess import Popen, PIPE, STDOUT
import random

from listener import setuid, unsetuid
import univention.debug as ud

name = "bareos"
description = "Generate Bareos Configuration for Clients"
filter = "(objectClass=bareosClientHost)"
attributes = []

PATH_PREFIX = "/etc/bareos/autogenerated"
JOBS_PATH = PATH_PREFIX + "/clients"
INCLUDES_PATH = PATH_PREFIX + "/clients.include"
BCONSOLE_CMD = ["/usr/bin/bconsole"]

JOB_DISABLED = "Not"

bareos_gid = grp.getgrnam("bareos").gr_gid


def getFqdn(entry):
    if not entry.has_key("cn"):
        return None

    name = entry["cn"][0]
    if entry.has_key("associatedDomain"):
        name = name + "." + entry["associatedDomain"][0]

    return name


def initialize():
    """Initialize the module once on first start or after clean."""
    ud.debug(ud.LISTENER, ud.INFO, "BAREOS: Initialize")


def handler(dn, new, old):
    """Handle changes to 'dn'."""
    setuid(0)
    try:
        # if configRegistry['server/role'] != 'domaincontroller_master':
        #       return

        # ud.debug(ud.LISTENER, ud.INFO, 'BAREOS: handler '+dn+' '+str(bareos_gid))

        if new and not old:
            # changeType: add
            name = getFqdn(new)
            processClient(name, new)

        elif old and not new:
            # changeType: delete
            try:
                name = getFqdn(old)
                processClient(name, old, delete=True)
            except:
                pass
        else:
            # changeType: modify
            name = getFqdn(new)
            processClient(name, new)
    finally:
        unsetuid()


def clean():
    """Handle request to clean-up the module."""
    return


def postrun():
    """Transition from prepared-state to not-prepared."""
    return


def processClient(client_name, entry, delete=False):
    if client_name is None:
        return

    client_type = "generic"
    if "univentionWindows" in entry["objectClass"]:
        client_type = "windows"

    if delete == True:
        removeClient(client_name, client_type)
        return

    if entry.has_key("bareosEnableJob"):
        if entry["bareosEnableJob"][0] == JOB_DISABLED:
            removeClient(client_name, client_type)
            return

    addClient(client_name, client_type)


def addClient(client_name, client_type):
    createClientJob(client_name, client_type)
    addClientInclude(client_name)
    exportBareosFdDirectorResource(client_name, client_type)


def removeClient(client_name, client_type):
    if client_name == None:
        return
    disableClientJob(client_name, client_type)
    addClientInclude(client_name)


def getClientSecret(client_name):
    path = getClientSecretPath(client_name)
    password = None

    try:
        f = open(path, "r")
        password = f.read().strip()
    except:
        password = createClientSecret(client_name)

    return password


def exportBareosFdDirectorResource(client_name, client_type):
    # send commands via pipe to bconsole
    process = Popen(BCONSOLE_CMD, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
    # additional reload required, to gurantee that client is known to director
    out = process.communicate(
        b'reload\nconfigure export client="{client_name}-fd"\n'.format(
            client_name=client_name
        )
    )[0]
    ud.debug(ud.LISTENER, ud.INFO, "bareos export output:\n" + str(out))


def createClientSecret(client_name):
    path = getClientSecretPath(client_name)

    char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase
    password = "".join(random.sample(char_set * 40, 40))
    with os.fdopen(
        os.open(path, os.O_CREAT | os.O_WRONLY, stat.S_IRUSR | stat.S_IWUSR), "w"
    ) as f:
        f.write(password)
    os.chown(path, -1, 0)

    return password


def removeClientJob(client_name):
    path = JOBS_PATH + "/" + client_name + ".include"
    os.remove(path)


def createClientJob(client_name, client_type, enable="Yes"):
    password = getClientSecret(client_name)
    path = JOBS_PATH + "/" + client_name + ".include"
    templatefile = JOBS_PATH + "/" + client_type + ".template"
    with open(templatefile, "r") as f:
        content = f.read()

    t = string.Template(content)
    with os.fdopen(
        os.open(
            path, os.O_CREAT | os.O_WRONLY, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP
        ),
        "w",
    ) as f:
        f.write(t.substitute(enable=enable, password=password, client_name=client_name))
    os.chown(path, -1, bareos_gid)


def disableClientJob(client_name, client_type):
    createClientJob(client_name, client_type, "No")


def getClientIncludePath(client_name):
    return "@" + JOBS_PATH + "/" + client_name + ".include"


def getClientSecretPath(client_name):
    return JOBS_PATH + "/" + client_name + ".secret"


def addClientInclude(client_name):
    # is the client already in the include list?
    if isClientIncluded(client_name):
        # update the timestamp on the file
        # to let the cron script know the configuration
        # has changed
        os.utime(INCLUDES_PATH, None)
        return

    # if not, add it at the end of the file
    with open(INCLUDES_PATH, "a") as f:
        f.write(getClientIncludePath(client_name))
        f.write("\n")


def isClientIncluded(client_name):
    want = getClientIncludePath(client_name)
    with open(INCLUDES_PATH, "r") as f:
        for l in f.readlines():
            if want in l:
                return True
    return False
