# -*- coding: utf-8 -*-
#
##########################################################################
# pyeole.diagnose - Diagnostic tools for EOLE
# Copyright © 2013-2014 Pôle de compétences EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
##########################################################################

from _socket import timeout as socket_timeout

try:
    from urllib2 import urlopen
    from urllib2 import Request
    from urllib2 import HTTPError
    from urllib2 import URLError
    RemoteDisconnected = URLError
except:
    from urllib.request import urlopen
    from urllib.request import Request
    from urllib.request import HTTPError
    from urllib.request import URLError
    from http.client import RemoteDisconnected

import time, datetime
import os
from os import environ
from os.path import isfile, exists
from shutil import which

import logging

from creole.client import CreoleClient
from creole.config import VIRTMASTER
from pyeole.process import system_code
from pyeole.process import system_out
from pyeole.process import tcpcheck

log = logging.getLogger(__name__)

OK = 0
NOK = 1
NOT_AVAILABLE = 2
CACHE_IPTABLES = '/etc/eole/iptables'
CACHE_IPSET = '/etc/eole/ipset'
BIN_IPTABLES_SAVE = '/sbin/iptables-save'
global activer_agregation
activer_agregation = None
SOCKETFILE = '/run/clamav/clamd.ctl'

try:
    import pyclamd
except:
    pyclamd = None
CLAMD_CONTAINER = '/etc/eole/clamd-reload.cnf'
FRESHCLAM_FILE = '/var/log/clamav/freshclam-status.log'
REPORT_MAJ_FILE = u'/var/lib/eole/reports/maj.log'
MAJ_SUCCES_LOCK = u'/var/lib/eole/majsuccess.lock'


LXC_START_FILES = '/etc/lxc/auto/*'

def test_http(url, use_proxy=False, timeout=5):
    """Testing if an HTTP URL is reachable

    :param url: URL to check
    :type url: `str`
    :param use_proxy: let urllib2 use proxy setting from environment
    :type use_proxy: `bool`
    :param timeout: connection timeout in seconds
    :type timeout: `int`
    :return: if an HTTP request success on the :data:`url`
    :rtype: `bool`

    """
    log.debug(u'Test if {0} is reachable'.format(url))
    http_proxy = None
    if not use_proxy:
        try:
            http_proxy = environ.pop('http_proxy')
        except KeyError:
            pass

    try:
        req = Request(url)
        reponse = urlopen(req, timeout=timeout)
        log.debug('OK')
        ret = True
    except (HTTPError, URLError, socket_timeout, RemoteDisconnected) as err:
        log.debug('KO: {} ({})'.format(type(err), err))
        ret = False

    if http_proxy is not None:
        # restore environment
        environ.update({'http_proxy': http_proxy})

    return ret


def compare_iptables(container=VIRTMASTER):
    """comparaison des iptables-saves pour le maitre
    et les conteneurs
    """
    client = CreoleClient()
    if container == VIRTMASTER:
        # comparaison du conteneur maitre
        container_name = container
        container_filename = CACHE_IPTABLES
    # comparaison du conteneur esclave
    else:
        container_name = client.get_creole('container_name_' + container)
        container_path = client.get_creole('container_path_' + container)
        container_filename = container_path + CACHE_IPTABLES
    ret_code = compare_iptables_one_file(container_filename, container_name)
    if ret_code != OK:
        return ret_code
    return OK


def compare_iptables_one_file(iptables_cache_fname, container=VIRTMASTER):
    """comparaison d'un fichier iptables-save

    :param iptables_fname: nom long d'un fichier iptables-save
    """
    global activer_agregation
    if not isfile(iptables_cache_fname) or not isfile(BIN_IPTABLES_SAVE):
        return(NOT_AVAILABLE)
    try:
        code, out, err = system_out([BIN_IPTABLES_SAVE], container=container)
    except:
        code = 1
    if code == 0:
        cache_iptables = set(open(iptables_cache_fname, 'r').read().splitlines())
        new_iptables = set(out.splitlines())
        for not_present in cache_iptables - new_iptables:
            if activer_agregation is None:
                client = CreoleClient()
                activer_agregation = client.get_creole('activer_agregation', 'non')
            if not_present[0] not in [':', '#'] and (activer_agregation == 'non' or "SNAT" not in not_present):
                return (NOK)
        #test les chaines et les politiques par défaut
        new_policy = set()
        for new in new_iptables:
            if new.startswith(':'):
                new_policy.add(new.split('[')[0])
        cache_policy = set()
        for cache in cache_iptables:
            if cache.startswith(':'):
                cache_policy.add(cache.split('[')[0])
        if cache_policy - new_policy:
            return (NOK)
    if code != 0:
        #print('impossible de lancer {0}'.format(BIN_IPTABLES_SAVE))
        return (NOK)
    return (OK)


def compare_ipset():
    """
    compare les regles ipsets

    **important** les regles ipsets ne sont pas verifiee sur les conteneurs
    car tout est sur le maitre pour l'instant
    (il faut que les namespaces ipsets soient implementes par conteneurs
    au niveau du noyau, ce qui n'est pas encore le cas pour l'instant)
    """
    if not isfile(CACHE_IPSET) or not which('ipset'):
        return(NOT_AVAILABLE)
    code, out, err = system_out(['ipset', 'save'])
    if code == 0:
        cache_iptables = set([i for i in open(CACHE_IPSET, 'r').read().splitlines() if "local_" not in i])
        new_iptables = set([i for i in out.splitlines() if "local_" not in i])
        if cache_iptables - new_iptables != set([]):
            return(NOK)
    else:
        print("Erreur à l'exécution de 'ipset save' : {0}".format(err))
        return(NOK)
    return(OK)


def test_freshclam():
    def _parse_file():
        if ret['STATUS'] == 0:
            now = datetime.datetime.now(tz=datetime.timezone.utc)
            last = datetime.datetime.fromtimestamp(ret['DATE'], tz=datetime.timezone.utc)
            one_week_ago = datetime.timedelta(weeks=1)
            last_update = time.strftime("%d %b %Y %H:%M:%S", time.localtime(ret['DATE']))
            if now - last < one_week_ago:
                status = 'On'
                msg = 'Base antivirale mise à jour le %s' % last_update
            else:
                status = 'Off'
                msg = "Base antivirale mise à jour le %s, soit il y a plus d'une semaine" % last_update
        else:
            status = 'Off'
            if ret['STATUS'] == 1:
                msg = "Impossible de mettre à jour la base de signature"
            else:
                msg = "Base antivirale à jour, mais freshclam n'a pas réussi à relancer clamd à l'issue de la mise jour"
        return status, msg

    ret = {}
    if isfile(FRESHCLAM_FILE) and os.stat(FRESHCLAM_FILE).st_size != 0:
        try:
            fh = open(FRESHCLAM_FILE)
            for line in fh.readlines():
                if line.startswith('STATUS='):
                    ret['STATUS'] = int(line.split('"')[1])
                elif line.startswith('DATE='):
                    ret['DATE'] = float(line.split('"')[1])
            status, msg = _parse_file()
        except Exception as e:
            status = 'Off'
            msg = "Impossible de lire %s : %s" % (FRESHCLAM_FILE, e)
    else:
        status = ''
        msg = "Le fichier %s n'existe pas ou est vide" % FRESHCLAM_FILE
    return {'msg': msg, 'status': status}


def test_pyclamd(only_ping=True):
    try:
        if pyclamd is None:
            return 1
        if not exists(SOCKETFILE):
            return 2
        clamd = pyclamd.ClamdUnixSocket(SOCKETFILE, timeout=30)
        clamd.ping()
        if only_ping:
            return 0
        clamd.reload()
        return 0
    except:
        print ('Could not ping or reload clamd')
        return 1


def test_clamd():
    if isfile(CLAMD_CONTAINER):
        try:
            for line in open(CLAMD_CONTAINER, 'r').readlines():
                if line.startswith('CLAMD_CONTAINER='):
                    containers = line.split('"')[1].split()
                    if containers == []:
                        #aucun service clam n'est activé
                        return ''
                    for container in containers:
                        if not container.isalnum():
                            msg = 'a container has an invalid name'
                            #fait une erreur dans le diagnose
                            #log.error(msg + ' ' + container)
                            raise Exception('a container has an invalid name')
                    break
        except Exception as err:
            #fait une erreur dans le diagnose
            #log.error('cannot read {0} file: {1}'.format(CLAMD_CONTAINER, err), exc_info=True)
            import traceback
            traceback.print_exc()
            return 1
        else:
            for container in containers:
                try:
                    code = system_code(['/usr/bin/clamd-eole', '-p'], container=container)
                except:
                    code = 1
                if code != 0:
                    return 1
            return 0
    else:
        return ''


def test_container(name, ip=None):
    """Test if a container is running and reachable

    Check the state with the ``lxc-info`` command.

    If :data:`ip` is provided and the state is OK, check if SSH port
    is open.

    :param name: name of the container
    :type name: `str`
    :return: if the container is running and reachable
    :rtype: `bool`

    """
    is_running = False
    code, out, err = system_out(['lxc-info', '--state', '-n', name])
    if code == 0:
        """
        lxc-info --state returns something like:
            State:   RUNNING
        """
        is_running = out.strip().endswith('RUNNING')

    if is_running and ip is not None:
        is_running = tcpcheck(ip, '22', '2')

    return is_running


def test_containers():
    """Check if the containers are running

    :return: The running state for each container
    :rtype: `dict`

    """
    client = CreoleClient()
    ret = {}
    for container in client.get_groups():
        if container in ['all', VIRTMASTER]:
            continue
        ip = client.get_creole('container_ip_{0}'.format(container))
        ret[container] = test_container(container, ip)

    return ret


def test_maj():
    def _parse_file():
        msg = ret['MSG']
        last = datetime.datetime.fromtimestamp(ret['DATE'], tz=datetime.timezone.utc)
        last_update = time.strftime("%d %b %Y %H:%M:%S", time.localtime(ret['DATE']))
        if ret['STATUS'] == 0:
            now = datetime.datetime.now(tz=datetime.timezone.utc)
            one_week_ago = datetime.timedelta(weeks=1)
            one_month_ago = datetime.timedelta(days=30)
            if now - last < one_week_ago:
                status = 'On'
            elif now - last > one_month_ago:
                status = 'Off'
                msg = "Dernière mise à jour il y a plus d'un mois"
            else:
                status = 'Warn'
                msg = "Dernière mise à jour il y a plus d'une semaine"
        elif ret['STATUS'] == 2:
            now = datetime.datetime.now(tz=datetime.timezone.utc)
            two_hours_ago = datetime.timedelta(hours=2)
            if now - last < two_hours_ago:
                status = 'On'
            else:
                status = 'Off'
                msg = "Mise à jour commencée il y a plus de deux heures"
        else:
            status = 'Off'
        msg += ' (état le %s)' % last_update
        return status, msg

    ret = {}
    if isfile(REPORT_MAJ_FILE) and os.stat(REPORT_MAJ_FILE).st_size != 0:
        try:
            fh = open(REPORT_MAJ_FILE)
            for line in fh.readlines():
                if line.startswith('STATUS='):
                    ret['STATUS'] = int(line.split('"')[1])
                elif line.startswith('DATE='):
                    ret['DATE'] = float(line.split('"')[1])
                elif line.startswith('MSG='):
                    ret['MSG'] = line.split('"')[1].rstrip()
            status, msg = _parse_file()
        except Exception as e:
            status = 'Off'
            msg = "Impossible de lire %s : %s" % (REPORT_MAJ_FILE, e)
    else:
        status = ''
        msg = "Le fichier %s n'existe pas ou est vide" % REPORT_MAJ_FILE
    return {'msg': msg, 'status': status}

def test_maj_reconfigure():
    status = {True: 'Off', False: 'On'}.get(isfile(MAJ_SUCCES_LOCK))
    if status == 'On':
        msg = 'OK'
    else:
        msg = 'le serveur est mis à jour mais sans reconfigure'
    return {'msg': msg, 'status': status}

def test_need_reboot():
    from creole.fonctionseole import controle_kernel
    return controle_kernel(False)
