# -*- coding: utf-8 -*-

import pwd
import re
import locale
import sys
from os import makedirs, unlink, stat
from os.path import join, isfile, isdir, ismount, exists, realpath
from pickle import dump, load
from time import sleep, time
from datetime import datetime, timedelta
from configobj import ConfigObj

if sys.version_info[0] >= 3:
    unicode = str
    open_mode = 'wb'
    read_mode = 'rb'
else:
    open_mode = 'w'
    read_mode = 'r'

try:
    import ConfigParser
except:
    from configparser import ConfigParser

from creole.client import CreoleClient, CreoleClientError
from creole.loader import creole_loader, config_save_values
from creole.template import CreoleTemplateEngine
from pyeole.service import manage_services
from pyeole.process import system_out, tcpcheck
from pyeole.ansiprint import print_title, print_orange

BAREOS_CONF = "/etc/eole/bareos.conf"
BCONSOLE_CONF = '/etc/bareos/bconsole.conf'

INCLUDE_CONF = '/etc/bareos/include-options.conf'
RESTORE_CONF = '/etc/bareos/bareos-restore.conf'

BAREOS_RAPPORT = '/var/lib/eole/reports/resultat-bareos'
BAREOS_RAPPORT_FSL = '/var/lib/eole/reports/resultat-bareos-freespace'
BAREOS_SUPPORT = '/etc/eole/extra/bareos/config.eol'
MOUNT_POINT = '/mnt/sauvegardes'
TEST_MOUNT_FILE = join(MOUNT_POINT, 'eole_test.txt')
FILE_LOG_BACKUP = '/var/log/rsyslog/local/bareos-dir/bareos-dir.info.log'

BAREOS_TEMPLATES = ['bareosschedulepre.conf',
                    'bareosschedule.conf',
                    'bareosschedulepost.conf',
                    'bareos-dir.conf',
                    'bareos_ead_extra.conf',
                    'bareosschedule_remote.conf',
                    'bareos_ead_extra_remote.conf']

BAREOS_RAPPORT_OK = "1"
BAREOS_RAPPORT_ERR = "-1"
BAREOS_RAPPORT_UNKNOWN = "0"

client = CreoleClient()

class Disabled(Exception):
    pass

def bareos_configured():
    """
        Teste si bareos est configuré
    """
    if bareos_active_sd():
        sup = load_bareos_support()
        if sup['support_type'] == 'none':
            return False
    if bareos_active_dir():
        sup = load_bareos_mail()
        if not load_bareos_mail:
            return False
    return True

def bareos_active_sd():
    return u'oui' == client.get_creole('activer_bareos_sd')

def bareos_active_dir():
    return u'oui' == client.get_creole('activer_bareos_dir')

def load_bareos_conf(filename):
    cfgparser = ConfigParser.ConfigParser()
    dic = {}
    cfgparser.read(filename)
    for section in cfgparser.sections():
        dic[section] = eval(cfgparser.get(section, 'val'))[0]
    return dic

def reload_conf(test='both'):
    if test not in ['both', 'sd', 'dir']:
        raise Exception(u'ERREUR : test doit être "both", "sd" ou "dir')
    #both + dir
    if test != 'sd':
        cmd = ['/usr/sbin/bareos-dir', '-t']
        ret, stdout, stderr = system_out(cmd)
        if ret != 0:
            raise Exception(u'ERREUR : erreur au test de la configuration : {0}, {1}'.format(stdout, stderr))
        run_bareos_cmd('reload')
    #both + sd
    if test != 'dir':
        cmd = ['/usr/sbin/bareos-sd', '-t']
        ret, stdout, stderr = system_out(cmd)
        if ret != 0:
            raise Exception(u'ERREUR : erreur au test de la configuration : {0}, {1}'.format(stdout, stderr))
        manage_services('restart', 'bareos-storage')

def _save_bareos_conf(filename, dic):
    fh = open(filename, 'w')
    cfgparser = ConfigParser.ConfigParser()
    for section, val in dic.items():
        cfgparser.add_section(section)
        if type(val) == list:
            cfgparser.set(section, 'val', val)
        else:
            cfgparser.set(section, 'val', [val])
        cfgparser.set(section, 'valprec', [])
        cfgparser.set(section, 'valdefault', [])
    cfgparser.write(fh)
    fh.close()

def unicode_or_none(value, multi=False):
    """
    Préparation d'une valeur de type unicode
    pour l'injecter dans Tiramisu
    :multi: boléen vaut True si la variable est multivaluée
    """
    if multi:
        if value in ('', None):
            return []
        return unicode(value).split(',')
    if value in ('', None):
        return None
    return unicode(value)

def system_out_catched(cmd):
    """
    Retourne une exception avec le message d'erreur en cas
    de sortie non nulle.

    :param cmd: commande à exécuter
    :type cmd: str
    """

    code, result, stderr = system_out(cmd)
    if code != 0:
        raise OSError(code, stderr)
    else:
        return result

def parse_mounts():
    """
    Retourne un dictionnaire des points de montage.

    Les entrées sont de la forme :
    point de montage : {nœud,
                        type de système de fichiers,
                        options de montage}
    """

    mount_output_re = r'(?P<node>.*?) (on )?(?P<mount_point>.*?) (type )?(?P<fs_type>.*?) \((?P<options>.*)\).*'
    mounts_list = [re.search(mount_output_re, mount).groupdict()
            for mount in system_out_catched(['/bin/mount']).split('\n')
            if mount != '']
    mounts = {}
    for mount in mounts_list:
        mounts[mount['mount_point']] = {'node': mount['node'],
                    'fs_type': mount['fs_type'],
                    'options': mount['options'].replace(')','').split(',')}
    return mounts

def test_mount_point(dic):
    """
    Retourne l'état du point de montage parmi l'un des trois états suivants :
    1) point de montage libre --> montage
    2) point de montage occupé par le bon périphérique avec les bons paramètres --> rien à faire
    3) point de montage occupé par le mauvais périphérique ou le bon périphérique avec les mauvais paramètres --> démontage puis montage

    Il y a deux paramètres à vérifier :
    - l'identité du périphérique monté (adresse, contenu)
    - les droits associés à ce périphérique

    :param dic: paramètres de bareos saisis dans l'ead ou avec bareosconfig -s
    :type dic: dictionnaire
    """

    # drapeaux
    mount_point_busy = False
    right_node_mounted = False
    right_rights = False
    # test de l'identité du périphérique monté (drapeaux mount_point_busy et
    # right_node_mounted)
    mounts = parse_mounts()
    if MOUNT_POINT in mounts:
        mount_point_busy = True
        if dic['support_type'] == 'usb' and \
           mounts[MOUNT_POINT]['node'] == realpath(dic['usb_path']):
            right_node_mounted = True
        elif dic['support_type'] == 'smb' and \
           dic['smb_machine'] in mounts[MOUNT_POINT]['node']:
            right_node_mounted = True
        elif dic['support_type'] == 'manual':
            right_node_mounted = True
        else:
            right_node_mounted = False
    elif dic['support_type'] == 'manual':
        right_node_mounted = True
        mount_point_busy = True

    # test d'écriture si le montage est le bon (drapeau right_rights)
    if right_node_mounted is True: # implique mount_point_busy == True
        try:
            system_out_catched(['su', '-', 'bareos', '-c', "touch {0}".format(TEST_MOUNT_FILE), '-s', '/bin/bash'])
            system_out_catched(['su', '-', 'bareos', '-c', "rm -f {0}".format(TEST_MOUNT_FILE), '-s', '/bin/bash'])
            right_rights = True
        except OSError as e:
            if isfile(TEST_MOUNT_FILE):
                system_out_catched('rm -f {0}'.format(TEST_MOUNT_FILE))

    return {'is_busy': mount_point_busy,
            'is_right_node': right_node_mounted,
            'has_rights': right_rights}


def mount_status_to_str(status):
    """
    Retourne le statut du support de sauvegarde pour affichage.
    :param status: résultat de la commande test_mount_point.
    :type status: dict
    """
    messages = {'is_right_node': 'point de montage',
                'has_rights': 'permissions',
                'is_busy': 'montage',
                'manual': 'Pas de montage en mode manuel'}

    message = '\n'.join([messages[st] + " : OK" for st in status.keys() if status[st] == True] +
            [messages[st] + " : Erreur" for st in status.keys() if not status[st] == True])
    if not status['has_rights']:
        message += " (l'utilisateur bareos doit avoir les droits en écriture sur les fichiers de sauvegarde)"
    return message

def parse_custom_mount_file(conf):
    """
    Retourne les commandes de montage présentes dans le fichier conf et
    les paramètres si possible.

    Les commandes de montage sont filtrées pour extraire les paramètres
    de montage et enlever les options qui empêchent le fonctionnement
    de test_mount_point (-n qui ne créé pas d'entrée dans /etc/mtab).

    :param conf: chemin du fichier
    :type conf: str
    """

    conf = ConfigObj(conf)
    return conf

def get_mount_command(dic, uid):
    """
    Renvoie la commande pour monter le support en donnant la priorité
    au fichier BAREOS_CONF si il existe.


    :param dic: paramètres de bareos
    :type dic: dictionnaire
    """

    bareos_config = parse_custom_mount_file(BAREOS_CONF)

    if dic['support_type'] == 'usb':
        #si usb_path est un lien symbolique, suivre le lien (#2447)
        usb_path = realpath(dic['usb_path'])
        USB_MOUNT = bareos_config.get('USB_MOUNT', None)
        if USB_MOUNT is None:
            #if USB_MOUNT not set:
            #if volume if in VFAT, add uid option
            code, vtype, stderr = system_out(['/sbin/blkid', '-sTYPE', '-ovalue', usb_path])
            if code != 0:
                if stderr:
                    raise Exception(u'Erreur avec blkid : {0}'.format(stderr))
                else:
                    raise Exception(u'ERREUR : périphérique {0} non reconnu'.format(usb_path))
            if vtype.rstrip() in ['vfat', 'ntfs']:
                USB_MOUNT = '/bin/mount {0} {1} -o noexec,nosuid,nodev,uid={2},umask=0077'
            else:
                USB_MOUNT = '/bin/mount {0} {1} -o noexec,nosuid,nodev'
                # chown_mount_point = True

        cmd = USB_MOUNT.format(usb_path, MOUNT_POINT, uid).split()

    elif dic['support_type'] == 'smb':

        ret = tcpcheck(dic['smb_ip'], '139', 2)
        if not ret:
            ret = tcpcheck(dic['smb_ip'], '445', 2)
            if not ret:
                raise Exception(u"La machine {0} n'est pas accessible sur les ports 139 et 445".format(dic['smb_ip']))
        DISTANT_LOGIN_MOUNT = bareos_config.get('DISTANT_LOGIN_MOUNT', '/bin/mount -t cifs -o username={0},password={1},ip={2},uid={3},noexec,nosuid,nodev,vers=3.0 //{4}/{5} {6}')
        DISTANT_MOUNT = bareos_config.get('DISTANT_MOUNT', '/bin/mount -t cifs -o password={0},ip={1},uid={2},noexec,nosuid,nodev,vers=3.0 //{3}/{4} {5}')
        if dic['smb_login'] is not None:
            cmd = DISTANT_LOGIN_MOUNT.format(dic['smb_login'],
                    dic['smb_password'], dic['smb_ip'], uid,
                    dic['smb_machine'], dic['smb_partage'], MOUNT_POINT).split()
        else:
            cmd = DISTANT_MOUNT.format(dic['smb_password'], dic['smb_ip'], uid,
                    dic['smb_machine'], dic['smb_partage'], MOUNT_POINT).split()
    return cmd

def mount_bareos_support(retry=40, chown=False):
    """
    Monte le support configuré via l'ead ou bareosconfig.py -s
    sur le point de montage MOUNT_POINT.
    Renvoie Vrai si le montage est effectif, Faux sinon.

    :param retry: nombre d'essais pour le montage usb
    :type retry: entier
    """
    attempt = retry
    is_mounted = False
    # créé le répertoire MOUNT_POINT si il n'existe pas
    if not isdir(MOUNT_POINT):
        makedirs(MOUNT_POINT)

    #quitte si manual ou pas configuré
    dic = load_bareos_support()
    if dic['support_type'] == 'manual':
        mount_point_status = test_mount_point(dic)
        if chown == True and mount_point_status['has_rights'] == False:
            try:
                system_out_catched(['chown', 'bareos:tape', MOUNT_POINT])
            except Exception as e:
                print(unicode(e))
            else:
                mount_point_status['has_rights'] = True
        return False not in mount_point_status.values(), mount_point_status

    if dic['support_type'] == 'none':
        raise Exception("ERREUR : bareos n'est pas configuré")

    # configure l'uid à utiliser pour le montage
    try:
        uid = pwd.getpwnam('bareos').pw_uid
    except:
        uid = 102

    # démonte et monte seulement si le
    # point de montage est occupé par un autre périphérique
    mount_point_status = test_mount_point(dic)
    if mount_point_status['is_busy'] == True:
        if mount_point_status['is_right_node'] == True:
            if mount_point_status['has_rights'] == True:
                is_mounted = True
            elif chown == True:
                try:
                    system_out_catched(['chown', 'bareos:tape', MOUNT_POINT])
                except Exception as e:
                    print(unicode(e))
                else:
                    is_mounted = True
            else:
                umount_bareos_support()
        else:
            umount_bareos_support()
    if is_mounted == False and attempt > 0:
        # récupère la commande à utiliser pour monter
        try:
            cmd = get_mount_command(dic, uid)
            system_out_catched(cmd)
        except Exception as error:
            print("Problème de montage ({0} essais restants)\n{1}".format(attempt-1, error))
            umount_bareos_support()
            sleep(3)
        finally:
            return mount_bareos_support(retry=attempt-1, chown=chown)
    return is_mounted, mount_point_status


def umount_bareos_support(volume='', error=False):
    dic = load_bareos_support()
    if isfile(volume) == True:
        volume = volume.split('-')[-2]
    _bareos_pcent_usage()
    test_space_left()
    if dic['support_type'] in ['manual', 'none'] or volume in ['inc', 'full', 'diff', '/var/lib/bareos/']:
        return True
    #if mount several time
    while ismount(MOUNT_POINT):
        cmd = ['/bin/umount', MOUNT_POINT]
        ret, stdout, stderr = system_out(cmd)
        if ret != 0:
            if error:
                raise Exception(u'ERREUR : démontage impossible : {0}, {1}'
                        u''.format(stdout, stderr))
            else:
                return True
    return True

def device_is_mount(device):
    if not exists(device):
        raise Exception(u"ERREUR : le périphérique {0} n'existe pas".format(device))
    cmd = ['grep', '-q',  '^{0} '.format(device), '/etc/mtab']
    ret = system_out(cmd)
    return ret[0] == 0


def get_queries():
    """
    Return dictionnary of queries (conventionnal name
    and order of appearance in /etc/bareos/query.sql)
    """
    query_header = re.compile(r'^:')
    eole_query = re.compile(r'\(EOLE:(?P<query>\w+)\)')
    with open('/etc/bareos/query.sql', 'r') as query_file:
        queries_list = [query for query in query_file.readlines()
                        if query_header.match(query)]
    queries = {}
    for i in range(len(queries_list)):
        query = eole_query.search(queries_list[i])
        if query:
            queries[query.group('query')] = i + 1
    return queries


def run_bareos_cmd(stdin):
    cmd = ['bconsole', '-c', BCONSOLE_CONF]
    return system_out(cmd, stdin=stdin)


def bareos_query(query, *args, empty_not_allowed=False):
    """
    Return bareos query command result
    :param query: query id as specified in /etc/bareos/query.sql
    :type query: str
    :param args: list of parameters needed for query
    :type args: list
    """
    queries = get_queries()
    try:
        query_num = queries[query]
    except KeyError:
        raise Exception('La liste des fonctions disponibles est la suivante : \n{0}'.format('\n'.join(queries.keys())))
    results_list = []
    params = '\n'.join(args) + '\nquit'
    status, res, err = run_bareos_cmd('query\n{0}\n{1}\n'.format(query_num, params))
    bareos_query_re = re.compile(r'\+(-+\+)+')
    try:
        resu = bareos_query_re.search(res)
        if resu:
            res = res.split(resu.group())
            header = [title.strip() for title in res[1].split('|') if title != '\n']
            results = [[j.strip() for j in i.split('|') if j != ''] for i in res[2].split('\n') if i != '']
            for result in results:
                result_dict = {}
                for value, key in zip(result, header):
                    result_dict[key] = value
                results_list.append(result_dict)
    except AttributeError:
        raise Exception('Résultat de la commande bconsole imprévu')
    if empty_not_allowed and not results_list:
        raise Exception('Aucun résultat\nstatus : {}\nstdout : {}\nstderr : {}'.format(status, res, err))
    return results_list


def _empty_file(filename):
    if isfile(filename):
        unlink(filename)
    fh = open(filename,'w')
    fh.write('')
    fh.close()

def run_backup(level='Full', when='', bareos_fd=None):
    """
    Lancement de la sauvegarde
    :param level: type de la sauvegarde
    :type level: str
    :param when: heure de la sauvegarde
    :type when: str
    """
    if level not in ['Full', 'Incremental', 'Differential']:
        raise Exception('Level {0} inconnu'.format(level))
    if client.get_creole('bareos_dir_use_local_sd') != 'non' and \
       load_bareos_support()['support_type'] == 'none':
        raise Exception("Le support de sauvegarde local n'est pas configuré")
    if when != '':
        if re.match(r'[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}', when) is None:
            raise Exception('Horaire mal renseigné')
        else:
            when = ' When=\"{0}\"'.format(when)
    if bareos_fd is None:
        bareos_fd = ''
    ret, stdout, stderr = run_bareos_cmd("run JobSchedule{1}Pre Level=Full{0}\n\n".format(when, bareos_fd))
    if ret != 0:
        raise Exception('ERREUR : de sauvegarde {0} : {1}, {2}'.format(level, stdout, stderr))
    ret, stdout, stderr = run_bareos_cmd("run JobSauvegarde{2} Level={0}{1}\n\n".format(level, when, bareos_fd))
    if ret != 0:
        raise Exception(u'ERREUR : de sauvegarde {0} : {1}, {2}'.format(level, stdout, stderr))
    #run catalog backup every time
    ret, stdout, stderr = run_bareos_cmd("run BackupCatalog level=Full{0}\n\n".format(when))
    if ret != 0:
        raise Exception(u'ERREUR : de sauvegarde du catalogue {0} : {1}, {2}'.format(level, stdout, stderr))
    return "Sauvegarde {0} lancée\nVous pouvez suivre son évolution dans le fichier {1}".format(level, FILE_LOG_BACKUP)

# -------------------------------------------------
# -------------- Bareos support -------------------
# -------------------------------------------------
def load_bareos_support():
    if not bareos_active_sd():
        raise Disabled(u'Bareos sd désactivé')
    return client.get('bareos.support')

def save_bareos_support_usb(usb_path, no_reload=False):
    if not bareos_active_sd():
        raise Disabled(u'Bareos sd désactivé')
    if usb_path is None:
        raise Exception(u'Chemin usb inconnu')
    config = creole_loader(load_extra=True, rw=True, owner='bareos')
    config.bareos.support.support_type = u'usb'
    config.bareos.support.usb_path = unicode(usb_path)
    if not config_save_values(config, 'bareos'):
        raise Exception(u"Le fichier de configuration n'a pas été enregistré")
    if not no_reload:
        write_bareos_schedule()

def save_bareos_support_smb(smb_machine, smb_ip,
        smb_partage, smb_login='', smb_password='', no_reload=False):
    if not bareos_active_sd():
        raise Disabled(u'Bareos sd désactivé')

    if smb_machine is None or smb_ip is None or smb_partage is None:
        raise Exception(u'Nom machine, IP ou nom du partage inconnu')

    config = creole_loader(load_extra=True, rw=True, owner='bareos')
    config.bareos.support.support_type = u'smb'
    smb_login = unicode_or_none(smb_login)
    smb_password = unicode_or_none(smb_password)
    if smb_login is not None and smb_password is None:
        raise Exception(u'Login spécifié mais password absent')
    if smb_login is None and smb_password is not None:
        raise Exception(u'Password spécifié mais login absent')

    config.bareos.support.smb_machine = unicode(smb_machine)
    config.bareos.support.smb_ip = unicode(smb_ip)
    config.bareos.support.smb_partage = unicode(smb_partage)
    # non mandatory variables
    config.bareos.support.smb_login = smb_login
    config.bareos.support.smb_password = smb_password
    if not config_save_values(config, 'bareos'):
        raise Exception(u"Le fichier de configuration n'a pas été enregistré")
    if not no_reload:
        write_bareos_schedule()

def save_bareos_support_manual(no_reload=False):
    if not bareos_active_sd():
        raise Disabled(u'Bareos sd désactivé')
    config = creole_loader(load_extra=True, rw=True, owner='bareos')
    config.bareos.support.support_type = u'manual'
    if not config_save_values(config, 'bareos'):
        raise Exception(u"Le fichier de configuration n'a pas été enregistré")
    if not no_reload:
        write_bareos_schedule()


def test_tmp_pcent():
    cmd = ['df', '/tmp', '--output=pcent']
    ret, stdout, stderr = system_out(cmd)
    if ret != 0:
        raise Exception(u"Impossible de vérifier l'espace disponible dans /tmp")
    line = stdout.split()
    if not line or not line[-1].endswith('%'):
        raise Exception(u"Impossible de retrouver l'espace disponible dans /tmp")
    if len(line[-1]) > 3:  # 100%
        raise Exception(u'Espace disponible dans /tmp trop juste')

def test_space_left():
    bareos_preserved_size = client.get_creole('bareos_preserved_size', 0) * pow(1024, 3)
    cmd = ['/bin/df', MOUNT_POINT, '--output=used,size']
    ret, stdout, stderr = system_out(cmd)
    if ret != 0:
        raise Exception(u"Impossible de vérifier l'espace occupé dans " + MOUNT_POINT)
    line = stdout.strip().split('\n')
    unit = line[0].split()[-1]
    multiplier = {'1K': 1024}[unit]

    print_title('Espace disponible')
    mnt_used_space, mnt_total_space = [int(space.strip()) * multiplier for space in line[-1].split()]
    mnt_free_space = mnt_total_space - bareos_preserved_size - mnt_used_space
    vols = bareos_query('space_left')
    recycle_free_space = 0
    for vol in vols:
        if vol['volstatus'] == 'Append':
            pass
        else:
            recycle_free_space += int(vol['maxvolbytes'].replace(',',''))

    total_free_space = mnt_free_space + recycle_free_space
    available_space = [
    "Espace {} sur le support : {} ({}{})".format("disponible" if bareos_preserved_size == 0 else "alloué",
                                                   sizeof_fmt(mnt_free_space),
                                                   mnt_free_space,
                                                   f", réserve de {sizeof_fmt(bareos_preserved_size)}" if bareos_preserved_size >0 else ""),
    "Espace disponible pouvant être réutilisé : {} ({})".format(sizeof_fmt(recycle_free_space), recycle_free_space),
    "Espace disponible total : {} ({})".format(sizeof_fmt(total_free_space), total_free_space)
    ]

    for s in available_space:
        print(s)


    print_title("Dernière estimation de la sauvegarde Totale :")
    estimate_output_date = None
    full_size_estimate_msg = None
    if isfile(BAREOS_RAPPORT_FSL):
        with open(BAREOS_RAPPORT_FSL) as f:
            data = f.read()
        if ' : ' in data:
            estimate_output, estimate_output_date = data.split(' : ')
            if estimate_output and estimate_output != "None":
                full_size_estimate = int(estimate_output.replace(',',''))
                full_size_estimate_msg = "{} ({}) Calculé le {}".format(sizeof_fmt(full_size_estimate), full_size_estimate, estimate_output_date)

    if full_size_estimate_msg is None:
        print_orange("Aucune estimation disponible : lancer le script /usr/share/eole/sbin/bareosfreespaceleft.py pour la calculer.")
        full_size_estimate = 0
    else:
        print(full_size_estimate_msg)


    print_title("Tailles des dernières sauvegardes :")

    last_full_size = bareos_query('last_full_backups')
    try:
        last_full_size = int(last_full_size[0]['result'].replace(',',''))
    except:
        last_full_size = 0
    last_full_size_msg = "Totale (dernière) : {} ({})".format(sizeof_fmt(last_full_size), last_full_size)

    avg_last_diff = bareos_query('avg_last_diff_backups')
    try:
        avg_last_diff = float(avg_last_diff[0]['result'].replace(',',''))
    except:
        avg_last_diff = 0
    avg_last_diff_msg = "Différentielle (moyenne) : {} ({})".format(sizeof_fmt(avg_last_diff), avg_last_diff)

    avg_last_incr = bareos_query('avg_last_incr_backups')
    try:
        avg_last_incr = float(avg_last_incr[0]['result'].replace(',',''))
    except:
        avg_last_incr = 0
    avg_last_incr_msg = "Incrémentale (moyenne): {} ({})".format(sizeof_fmt(avg_last_incr), avg_last_incr)


    print(last_full_size_msg)
    print(avg_last_diff_msg)
    print(avg_last_incr_msg)

    dic = load(open(BAREOS_RAPPORT, read_mode))
    try:
        dic = load(open(BAREOS_RAPPORT, read_mode))
    except (EOFError, IOError):
        dic = {}

    dic['free_space'] = {'total_free_space': total_free_space, 'total_free_spaceH': sizeof_fmt(total_free_space)}
    dic['backup_size'] = {'last_full': full_size_estimate, 'last_fullH': sizeof_fmt(full_size_estimate),
                         'avg_last_diff': avg_last_diff, 'avg_last_diffH': sizeof_fmt(avg_last_diff),
                         'avg_last_incr': avg_last_incr, 'avg_last_incrH': sizeof_fmt(avg_last_incr)}
    dump(dic, open(BAREOS_RAPPORT, open_mode), protocol=2)


def sizeof_fmt(num, suffix='B'):
    for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
        if abs(num) < 1024.0:
            return "%3.1f%s%s" % (num, unit, suffix)
        num /= 1024.0
    return "%.1f%s%s" % (num, 'Yi', suffix)

def test_bareos_support(force=False, chown=False):
    """
    :param force: drapeau pour forcer l'exécution du test
    :type force: booléen
    """

    if force or bareos_active_sd():
        status, dic = mount_bareos_support(retry=2, chown=chown)
        if status is True:
            umount_bareos_support()
        comment = mount_status_to_str(dic)
        test_tmp_pcent()
    else:
        status = True
        comment = 'Support non testé'
    return status, comment


# -------------------------------------------------
# ----------------- Bareos mail -------------------
# -------------------------------------------------
def load_bareos_mail():
    return client.get('bareos.mail')

def save_bareos_mail(mail_ok=None, mail_error=None, no_reload=False):
    if not bareos_active_dir():
        raise Disabled(u'Bareos dir désactivé')
    if mail_ok == mail_error == None:
        raise Exception(u"Aucune adresse mail à modifier")
    config = creole_loader(load_extra=True, rw=True, owner='bareos')
    if mail_ok is not None:
        config.bareos.mail.mail_ok = unicode_or_none(mail_ok, multi='True')
    if mail_error is not None:
        config.bareos.mail.mail_error = unicode_or_none(mail_error, multi='True')
    if not config_save_values(config, 'bareos'):
        raise Exception(u"Le fichier de configuration n'a pas été enregistré")
    if not no_reload:
        write_bareos_schedule()

# -------------------------------------------------
# ------------------ Bareos job -------------------
# -------------------------------------------------

DAY_TO_STRING = {1: 'dimanche au lundi', 2: 'lundi au mardi',
        3: 'mardi au mercredi', 4: 'mercredi au jeudi', 5: 'jeudi au vendredi',
        6: 'vendredi au samedi', 7: 'samedi au dimanche'}

DAY_TO_BAREOS = {1: 'mon', 2: 'tue', 3: 'wed', 4: 'thu', 5: 'fri',
                 6: 'sat', 7: 'sun'}
LEVEL_TO_FR = {'Incremental':'Incrémentale', 'Full':'Totale',
               'Differential': 'Différentielle'}

def load_bareos_jobs(): #check_active=True):
    """
    #check_active: don't load dicos to check if bareos-dir is activate
    """
    #if check_active and not bareos_active_dir():
    if not bareos_active_dir():
        raise Disabled(u'Bareos dir désactivé')
    return client.get('bareos.job.job_type')

#def save_bareos_job(dic):
#    if not bareos_active_dir():
#        raise Disabled(u'Bareos dir désactivé')
#    dump(dic, open(BAREOS_JOB, 'w'))

def write_bareos_schedule():
    """
    Write bareos schedule information.
    """
    engine = CreoleTemplateEngine()
    for tmpl in BAREOS_TEMPLATES:
        try:
            engine.instance_template(tmpl)
        except CreoleClientError:
            pass
    if not isfile(INCLUDE_CONF):
        fh = open(INCLUDE_CONF, 'w+')
        fh.write('\n')
        fh.close()
    if not isfile(RESTORE_CONF):
        fh = open(RESTORE_CONF, 'w+')
        fh.write('\n')
        fh.close()
    try:
        reload_conf(test='dir')
    except CreoleClientError:
        pass

def add_job(job, level, day, hour, end_day=None, no_reload=False):
    """
    job: weekly, daily or monthly
    level: Full/Incremental/Differential
    day: day in week
    hour: hour of job
    end_day: used only for daily job
    no_reload: don't reload bareos's configuration
    """
    if not bareos_active_dir():
        raise Disabled(u'Bareos dir désactivé')

    # check variables
    def is_valid_hour(hour):
        try:
            assert hour is not None
            validity = int(hour) in range(24)
        except (ValueError, AssertionError):
            validity = False
        return validity

    def is_valid_day(day):
        try:
            assert day is not None
            validity = int(day) in range(1, 8)
        except (ValueError, AssertionError):
            validity = False
        return validity

    def is_valid_day_range(day1, day2):
        if is_valid_day(day1) and is_valid_day(day2):
            return int(day2) > int(day1)

    def is_valid_level(level):
        return level in ['Full', 'Incremental', 'Differential']

    def is_valid_type(job_type):
        return job_type in ['daily', 'weekly', 'monthly']

    def required_args(job, level, day, hour, end_day):
        if not is_valid_type(job):
            raise Exception(u"Le type de sauvegarde (-j) est à choisir entre daily, weekly ou monthly.")
        if not is_valid_level(level):
            raise Exception(u"Le niveau de sauvegarde (--job_level) est à choisir entre Full, Incremental ou Differential.")
        if not is_valid_hour(hour):
            raise Exception(u"L'heure (--job_hour) est un entier à choisir dans l'intervalle [0, 23].")
        if not is_valid_day(day):
            raise Exception(u"Le jour (--job_day) est un entier à choisir dans l'intervalle [1, 7].")
        if job == 'daily':
            if not is_valid_day(end_day):
                raise Exception(u"Le jour (--job_end_day) est un entier à choisir dans l'intervalle [1, 7].")
            if not is_valid_day_range(day, end_day):
                raise Exception(u"Le jour de fin (--job_end_day) doit être supérieur au jour de début (--job_day).")

    required_args(job, level, day, hour, end_day)

    # check for duplicates
    def raise_if_conflict(day, index):
        raise Exception(u'Sauvegarde de la nuit du {0} entre en conflit avec la sauvegarde {1}'.format(DAY_TO_STRING[int(day)], index))

    jobs = load_bareos_jobs()
    for index in range(0, len(jobs['job_type'])):
        tjob_type = jobs['job_type'][index]
        tday = jobs['day'][index]
        tend_day = jobs['end_day'][index]
        #test si le jour du job est déjà utilisé pour le même type de job
        if unicode(day) == tday and unicode(job) == tjob_type:
            raise_if_conflict(day, index)
        #si le nouveau job est de type daily, il ne faut pas de weekly le
        #même jour
        if job == 'daily':
            if tjob_type == 'weekly' and day == tday:
                raise_if_conflict(day, index)
            #si le job existant est du type daily, vérifier la plage et pas
            #seulement le jour
            if tjob_type == 'daily':
                if tday < day <= tend_day:
                    raise_if_conflict(day, index)
        #si le job existant est de type daily, il faut verifié que le nouveau
        #job weekly ne soit pas définit le même jour
        elif tjob_type == 'daily' and job == 'weekly':
            if tday <= day <= tend_day:
                raise_if_conflict(day, index)

    # set variables in tiramisu object
    config = creole_loader(load_extra=True, rw=True, owner='bareos')
    config.bareos.job.job_type.job_type.append(unicode(job))
    config.bareos.job.job_type.level[-1] = unicode(level)
    config.bareos.job.job_type.day[-1] = unicode(day)
    config.bareos.job.job_type.hour[-1] = unicode(hour)
    if job == 'daily':
        config.bareos.job.job_type.end_day[-1] = unicode(end_day)
    if not config_save_values(config, 'bareos'):
        raise Exception(u"Le fichier de configuration n'a pas été enregistré")
    if not no_reload:
        write_bareos_schedule()


def del_job(jobnumber, no_reload=False):
    # shift job number to match python list range
    jobnumber = int(jobnumber) - 1
    # set variables in tiramisu object
    config = creole_loader(load_extra=True, rw=True, owner='bareos')
    config.bareos.job.job_type.job_type.pop(jobnumber)
    # write variables in config file
    if not config_save_values(config, 'bareos'):
        raise Exception(u"Le fichier de configuration n'a pas été enregistré")
    if not no_reload:
        write_bareos_schedule()


def display_bareos_jobs():
    if not bareos_active_dir():
        raise Disabled(u'Bareos dir désactivé')
    jobs = client.get('bareos.job.job_type')
    if jobs[u'job_type'] == []:
        return None
    disp = []
    for index in range(0, len(jobs['job_type'])):
        day = jobs['day'][index]
        level = LEVEL_TO_FR[jobs['level'][index]]
        hour = jobs['hour'][index]
        job = jobs['job_type'][index]
        message = "{0} : Sauvegarde {1} ".format(index+1, level.lower())
        if job == 'daily':
            end_day = jobs['end_day'][index]
            message += "de la nuit du {0} à la nuit du {1} ".format(DAY_TO_STRING[int(day)], DAY_TO_STRING[int(end_day)])
        elif job == 'weekly':
            message += "dans la nuit du {0} ".format(DAY_TO_STRING[int(day)])
        else:
            message += "dans la première nuit du mois du {0} ".format(DAY_TO_STRING[int(day)])
        message += "à {0}:00".format(hour)
        disp.append((index, message))
    return disp


def bareos_get_jobs_list():
    """
    returns a list of dictionnaries, 1 dictionnary per job
    """
    jobs = load_bareos_jobs()
    if jobs[u'job_type'] == []:
        return []
    job_list = []
    for index in range(0, len(jobs['job_type'])):
        day = jobs[u'day'][index]
        end_day = jobs['end_day'][index]
        level = jobs['level'][index]
        hour = jobs['hour'][index]
        job = jobs['job_type'][index]
        job_dict = {u'day': day, u'end_day': end_day, u'level': level, u'hour': hour, u'job': job}
        job_list.append(job_dict)
    return job_list


def pprint_bareos_jobs():
    jobs = display_bareos_jobs()
    if jobs is not None:
        pprinted_bareos_jobs = "\n".join(["\t{0}".format(job) for num, job in display_bareos_jobs()])
    else:
        pprinted_bareos_jobs = "\tAucun job programmé."
    return pprinted_bareos_jobs


def is_running_jobs():
    if not bareos_active_dir():
        raise Disabled(u"Bareos n'est pas actif")
    code, out, err = run_bareos_cmd('status dir')
    try:
        running_lines = out.split('\n====\n')[1]
    except:
        print("Erreur de status")
        print(out, err)
        return True

    if not 'No Jobs running.' in running_lines and \
        not 'Pas de job en cours.' in running_lines:
        print("Jobs en cours : Bareos est occupé")
        print("\n".join(running_lines.split('\n')[5:]))
        return True

    return False

def _date_from_epoch(epoch):
    locale.setlocale(locale.LC_TIME, '')
    return datetime.fromtimestamp(epoch).strftime("%A %d %B %Y à %H:%M")

def _is_more_than_one_day(epoch):
    return datetime.now() > datetime.fromtimestamp(epoch) + timedelta(1)

def _bareos_rapport(job_type, text, status):
    date = time()
    try:
        dic = load(open(BAREOS_RAPPORT, read_mode))
    except:
        dic = {}
    dic[job_type] = {'text' : text,
        'date': date,
        'status': str(status)}
    dump(dic, open(BAREOS_RAPPORT, open_mode), protocol=2)

def _bareos_pcent_usage():
    try:
        dic = load(open(BAREOS_RAPPORT, read_mode))
    except:
        dic = {}
    cmd = ['df', MOUNT_POINT, '--output=pcent,ipcent']
    ret, stdout, stderr = system_out(cmd)
    if ret != 0:
        pcent, ipcent = -1, -1
    else:
        value = stdout.split()
        if len(value) != 4:
            pcent, ipcent = -1, -1
        else:
            try:
                #remove '%' and return int
                pcent = int(value[2][:-1])
                ipcent = int(value[3][:-1])
            except:
                pcent, ipcent = -1, -1
    dic['disk'] = {'pcent': pcent, 'ipcent': ipcent}
    dump(dic, open(BAREOS_RAPPORT, open_mode), protocol=2)



def bareos_rapport_in_progress(job_type):
    _bareos_rapport(job_type, "Sauvegarde en cours depuis le {0}.", BAREOS_RAPPORT_UNKNOWN)

def bareos_rapport_ok(job_type):
    _bareos_rapport(job_type, "Sauvegarde terminée le {0}.", BAREOS_RAPPORT_OK)

def bareos_rapport_err(job_type):
    _bareos_rapport(job_type, "Sauvegarde échouée le {0}.", BAREOS_RAPPORT_ERR)

def bareos_rapport_load(report='sauvegarde'):
    if not isfile(BAREOS_RAPPORT) or stat(BAREOS_RAPPORT).st_size == 0:
        return (BAREOS_RAPPORT_UNKNOWN, 'Aucune sauvegarde')
    try:
        info = load(open(BAREOS_RAPPORT, read_mode))[report]
    except KeyError as e:
        return (BAREOS_RAPPORT_UNKNOWN, 'Aucune sauvegarde')
    except:
        return (BAREOS_RAPPORT_UNKNOWN, "Impossible de lire le rapport de sauvegarde")

    status = info['status']
    text = info['text'].format(_date_from_epoch(info['date']))
    if status == BAREOS_RAPPORT_UNKNOWN:
        #verifie si l'etat de sauvegarde n'a pas plus de 24 heures
        if _is_more_than_one_day(info['date']):
            status = BAREOS_RAPPORT_ERR
            text = "Sauvegarde démarrée depuis plus d'une journée (date de démarrage : %s)"% _date_from_epoch(info['date'])
    elif status not in [BAREOS_RAPPORT_UNKNOWN, BAREOS_RAPPORT_OK, BAREOS_RAPPORT_ERR]:
        status = BAREOS_RAPPORT_UNKNOWN
        text = 'Status inconnu : %s' % info['status']
    return (status, text)


def bareos_rapport_load_pcent_usage():
    if not isfile(BAREOS_RAPPORT) or stat(BAREOS_RAPPORT).st_size == 0:
        return (-1, -1)
    try:
        info = load(open(BAREOS_RAPPORT, read_mode))['disk']
    except:
        return (-1, -1)

    return (info.get('pcent', -1), info.get('ipcent', -1))

def bareos_rapport_load_free_space():

    status, text = "0", "0"

    if not isfile(BAREOS_RAPPORT) or stat(BAREOS_RAPPORT).st_size == 0:
        return ("-1", "pas suffisamment d'éléments pour faire une estimation")
    try:
        free_space = load(open(BAREOS_RAPPORT, read_mode))['free_space']
        backup_size = load(open(BAREOS_RAPPORT, read_mode))['backup_size']
    except:
        return ("-1", "pas suffisamment d'éléments pour faire une estimation")

    max_size = max(int(backup_size['last_full']), int(backup_size['avg_last_diff']), int(backup_size['avg_last_incr']))
    free_sp = int(free_space['total_free_space']) - max_size

    if free_sp <= 0:
        status = "-1"
        text = 'La taille de la sauvegarde prévue ({}) risque d\'être trop grande par rapport à l\'espace disponible ({})'.format(sizeof_fmt(max_size), free_space['total_free_spaceH'])
    else:
        status = "1"
        text = 'Espace disponible ({}) suffisant pour la sauvegarde'.format(free_space['total_free_spaceH'])
    return (status, text)
