#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2019:
# This file is part of Shinken Enterprise, all rights reserved.
import fnmatch
import json
import optparse
import subprocess
import httplib
import ssl
import base64

from shinkensolutions.color_print import *
from ssl import SSLError


ARCHITECTURE_EXPORT_CONFIG_FILE = "/etc/shinken/modules/architecture-export.cfg"
LINKS_RETENTION_FILE = "/var/lib/shinken/architecture_export_received.json"
NAGVIS_MAPS_PATH = "/etc/shinken/external/nagvis/etc/maps"
NAGVIS_ROTATION_PATH = "/etc/shinken/external/nagvis/etc/conf.d"


def list_architectures():
    with open(LINKS_RETENTION_FILE, 'r') as f:
        links = json.loads(f.read())
        if links:
            for arch_id in links:
                name = links[arch_id].get("name")
                print "* %-8s: %s" % ("Name", name)
                print "  %-8s: %s" % ("ID", arch_id)
                print
        else:
            print "No architectures were found"


def get_architecture_export_configuration():
    conf = {}
    with open(ARCHITECTURE_EXPORT_CONFIG_FILE, 'r') as f:
        for line in f:
            if not line.startswith("#"):
                m = re.match(r"\s*(?P<key>\w+)\s*(?P<value>.*)", line)
                if m is not None:
                    match = m.groupdict()
                    conf[match['key']] = match['value']
    return conf


def find_architecture_id(architecture_name):
    with open(LINKS_RETENTION_FILE, 'r') as f:
        links = json.loads(f.read())
        if links:
            for arch_id in links:
                name = links[arch_id].get("name")
                if name == architecture_name:
                    return arch_id
        
    raise Exception("Architecture %s not found" % architecture_name)


def find_architecture_name(architecture_id):
    with open(LINKS_RETENTION_FILE, 'r') as f:
        links = json.loads(f.read())
        if links:
            for arch_id in links:
                if architecture_id == arch_id:
                    return links[arch_id].get("name")
    
    raise Exception("Architecture %s not found" % architecture_id)


def get_listener_conf():
    architecture_export_conf = get_architecture_export_configuration()

    listener_login = architecture_export_conf.get('listener_login', 'Shinken')
    listener_password = architecture_export_conf.get('listener_password', 'Shinken')
    listener_use_ssl = (architecture_export_conf.get('listener_use_ssl', '0') == '1')
    listener_base_route = '/shinken/listener-shinken/v1/hosts/'

    return listener_login, listener_password, listener_use_ssl, listener_base_route


def get_listener_conn(synchronizer_address, listener_port, listener_use_ssl):
    if listener_use_ssl:
        if hasattr(ssl, 'SSLContext'):
            ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
            ssl_context.check_hostname = False
            ssl_context.verify_mode = ssl.CERT_NONE
            http_conn = httplib.HTTPSConnection(synchronizer_address, listener_port, timeout=3, context=ssl_context)
        else:
            http_conn = httplib.HTTPSConnection(synchronizer_address, listener_port, timeout=3)
    else:
        http_conn = httplib.HTTPConnection(synchronizer_address, listener_port, timeout=3)
    return http_conn


def get_listener_headers(listener_login, listener_pass):
    auth = base64.encodestring('%s:%s' % (listener_login, listener_pass)).replace('\n', '')
    listener_headers = {
        'Authorization': "Basic %s" % auth,
        "Content-type" : "application/json",
    }
    return listener_headers


def get_listener_hosts_todel(arch_name, conn, listener_base_route, listener_headers):
    try:
        conn.request("GET", listener_base_route, headers=listener_headers)
        response = conn.getresponse()
    except SSLError:
        raise Exception("Requesting the listener with HTTPS, but it failed. Verify if your listener-shinken uses HTTPS")
    except Exception as e:
        raise e
    if response.status != httplib.OK:
        # we need to fully read the response to be able to reuse the connection
        response.read()
        if response.status == httplib.FORBIDDEN:
            raise Exception("Bad login or password for the listener-shinken")
        raise httplib.HTTPException
    ret = json.loads(response.read())
    arch_listener_hosts = set()
    for host in ret:
        if host['host_name'].startswith(arch_name):
            arch_listener_hosts.add(host['_SE_UUID'])
    return arch_listener_hosts


def shutdown_arbiter():
    print "\n%s Shutting down Arbiter" % status("info")
    cmd = """/etc/init.d/shinken-arbiter stop"""
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    out = p.stdout.read()
    err = p.stderr.read()
    if err:
        print "%s Error while stopping Arbiter: %s" % (status("error"), err)
        return

    print "%s Arbiter stopped" % status("success")


# Deletes architecture links displayed in the Vizualisation UI
def delete_architecture_links(arch_id):
    with open(LINKS_RETENTION_FILE, 'r') as f:
        links = json.loads(f.read())
        
    if not arch_id in links:
        raise Exception("Architecture with ID '%s' has not been found in '%s'" % (arch_id, LINKS_RETENTION_FILE))
    
    del links[arch_id]

    with open(LINKS_RETENTION_FILE, 'w') as f:
        json.dump(links, f)


# Delete NagVis maps corresponding to the architecture
def delete_nagvis_maps(arch_id):
    
    print "\n%s Removing architecture NagVis maps..." % status("info")
    
    for map_file in os.listdir(NAGVIS_MAPS_PATH):
        if fnmatch.fnmatch(map_file, "*%s.cfg" % arch_id):
            os.remove("%s/%s" % (NAGVIS_MAPS_PATH, map_file))
            print "%s NagVis map '%s' has been removed" % (status("info"), map_file)

    print "%s Architecture NagVis maps has been removed" % status("success")
    print "\n%s Removing architecture NagVis rotations..." % status("info")
            
    for rotation_file in os.listdir(NAGVIS_ROTATION_PATH):
        if fnmatch.fnmatch(rotation_file, "*%s.ini.php" % arch_id):
            os.remove("%s/%s" % (NAGVIS_ROTATION_PATH, rotation_file))
            print "%s NagVis rotation '%s' has been removed" % (status("info"), rotation_file)

    print "%s Architecture NagVis rotations has been removed" % status("success")
    

def delete_listener_hosts_by_id(arch_id):
    try:
        delete_listener_hosts(find_architecture_name(arch_id))
    except Exception as e:
        raise e


def delete_listener_hosts(arch_name):
    listener_login, listener_password, listener_use_ssl, listener_base_route = get_listener_conf()
    listener_headers = get_listener_headers(listener_login, listener_password)
    conn = get_listener_conn('localhost', 7777, listener_use_ssl)

    try:
        todel_hosts = get_listener_hosts_todel(arch_name, conn, listener_base_route, listener_headers)
        
        while len(todel_hosts) > 0:
            host = todel_hosts.pop()
            
            conn.request("DELETE", "%s%s" % (listener_base_route, host), headers=listener_headers)
            response = conn.getresponse()

            if response.status != httplib.OK:
                # we need to fully read the response to be able to reuse the connection
                response.read()
                if response.status == httplib.FORBIDDEN:
                    raise Exception("Bad login or password for the listener-shinken")
                raise httplib.HTTPException

            ret = response.read()
            if ret != 'done':
                raise Exception(ret)
            
            print "%s Host '%s' has been removed" % (status("info"), host)
    except Exception as e:
        raise e
    

def delete_arch_by_id(id):
    shutdown_arbiter()
    delete_arch(id)
    

def delete_arch_by_name(name):
    shutdown_arbiter()
    
    try:
        arch_id = find_architecture_id(name)
    except Exception as e:
        print "%s Could not find architecture named %s (%s)" % (status("error"), name, str(e))
        sys.exit(1)
    
    delete_arch(arch_id)
    
    
def delete_arch(arch_id):
    removal_successful = True
    try:
        print "\n%s Removing architecture hosts from the listener-shinken..." % status("info")
        delete_listener_hosts_by_id(arch_id)
        print "%s Architecture hosts have been removed from the listener-shinken" % status("success")
    except Exception as e:
        removal_successful = False
        print "%s Could not delete the listener-shinken hosts for architecture: %s" % (status("error"), str(e))
    
    try:
        print "\n%s Removing architecture links from the Visualization UI..." % status("info")
        delete_architecture_links(arch_id)
        print "%s Architecture links have been removed from the Visualization UI" % status("success")
    except Exception as e:
        removal_successful = False
        print "%s Could not delete architecture links: %s" % (status("error"), str(e))
    
    try:
        delete_nagvis_maps(arch_id)
    except Exception as e:
        removal_successful = False
        print "%s Could not delete NagVis maps for architecture: %s" % (status("error"), str(e))
        
    if removal_successful:
        print "\n%s Architecture successfully removed" % (status("success"))
    else:
        print "\n%s Errors occurred during the removal process of the Architecture" % (status("error"))
    print "%s The Arbiter daemon has been shut down and must be manually restarted" % (status("warning"))


def status(message_type="success"):
    msg = ""
    color = ""
    if message_type == "success":
        msg = "OK"
        color = "green"
    elif message_type == "warning":
        msg = "WARNING"
        color = "yellow"
    elif message_type == "error":
        msg = "ERROR"
        color = "red"
    elif message_type == "info":
        msg = "INFO"
        color = "grey"
    
    return "[ %s ]" % sprintf("%-8s" % msg, color=color)


def confirm(msg):
    """
    Ask user to enter Y or N (case-insensitive).
    :return: True if the answer is Y.
    :rtype: bool
    """
    answer = ""
    while answer not in ["y", "n"]:
        answer = raw_input("%s [Y/n] " % msg).lower() or 'y'
        
    return answer == "y"


def main():
    parser = optparse.OptionParser("%prog ", description="Removes the specified generated architecture (maps and links in the Visualisation UI)")
    parser.add_option('-n', '--name', dest='name', default='', help='Name of the architecture to delete')
    parser.add_option('', '--id', dest='id', default='', help='Architecture ID to delete')
    parser.add_option('-l', '--list', dest='list', action="store_true", help='List generated architectures')
    parser.add_option('-f', '--force', dest='force', action="store_true", help='Do not ask confirmation before shutting down the Arbiter')
    
    opts, args = parser.parse_args()
    if opts.list:
        try:
            list_architectures()
            sys.exit(0)
        except Exception as e:
            print "%s Could not list architectures from %s (%s)" % (status("error"), LINKS_RETENTION_FILE, str(e))
            sys.exit(1)

    if opts.name and opts.id:
        print "%s The -n/--name and --id options are mutually exclusive" % status("error")
        sys.exit(1)
        
    if not opts.name and not opts.id:
        print "%s The name or ID of the architecture to delete must be passed with the -n/--name or --id options" % status("error")
        sys.exit(1)

    if opts.force:
        confirmation = True
    else:
        confirmation = confirm("Arbiter must be shut down before deleting an architecture. Do you want to continue ?")
    
    if confirmation:
        if opts.name:
            delete_arch_by_name(opts.name)
        elif opts.id:
            delete_arch_by_id(opts.id)
    else:
        print "%s Aborting architecture removal" % status("error")
        sys.exit(1)


if __name__ == '__main__':
    main()
