#!/usr/bin/python

# Copyright (C) 2013:
#    Gabes Jean, j.gabes@shinken-solutions.com
#
# This file is part of Shinken Enterprise, all rights reserved.

import sys
import os
import errno
import optparse
from tempfile import mkstemp
from shutil import move
from os import fdopen
import subprocess

import re

from shinkensolutions.localinstall import protect_stdout, unprotect_stdout, OK_COLOR, ERROR_COLOR

from shinken.synchronizer.synchronizerconfig import SynchronizerConfig
from shinken.synchronizer.dao.crypto import CRYPTO_NOT_TO_LOOK

from pymongo.connection import Connection

SYNCHRONIZER_OVERLOAD_FILE = "/etc/shinken-user/configuration/daemons/synchronizers/synchronizer_cfg_overload.cfg"
DEFAULT_UMASK = 0022
# Set umask to avoid problems when creating files
os.umask(DEFAULT_UMASK)


def get_conf(sync_config_files):
    # avoid printing the config files in stdout
    protect_stdout()
    conf = SynchronizerConfig()
    buf = conf.read_config(sync_config_files)
    raw_objects = conf.read_config_buf(buf)
    conf.split_sources_and_modules(raw_objects)
    conf.create_objects_for_type(raw_objects, 'synchronizer')
    conf.early_synchronizer_linking()
    conf.fill_default()
    unprotect_stdout()
    return conf


def get_mongo_db(conf):
    try:
        mongodb_uri = conf.mongodb_uri
        mongodb_database = conf.mongodb_database
        mongodb_con = Connection(mongodb_uri, fsync=True)  # force a fsync connection, so all access are forced even if it's slower on writes
        mongodb_db = getattr(mongodb_con, mongodb_database)
        return mongodb_db
    except:
        print "\033[31mERROR: Unable to connect to the mongo database to retrieve the data impacted by the fields protection\033[0m"
        sys.exit(1)


def show_data_to_proceed(conf, src_fields, additional_substrings=[], removal_substrings=[], quiet=False):
    dst_fields = src_fields.union(set(additional_substrings)) - set(removal_substrings)
    
    data_to_unprotect = set()
    data_to_protect = set()
    
    mongodb_db = get_mongo_db(conf)
    
    all_collection_names = mongodb_db.collection_names(include_system_collections=False)
    
    for collection_name in all_collection_names:
        if not collection_name.startswith('configuration'):
            continue
        collection = getattr(mongodb_db, collection_name)
        items = collection.find()
        for item in items:
            data_keys = [k.upper() for k in item.iterkeys() if k.startswith('_') and not k in CRYPTO_NOT_TO_LOOK]
            for data_name in data_keys:
                for to_unprotect in src_fields:
                    if to_unprotect in data_name:
                        data_to_unprotect.add(data_name)
                for to_protect in dst_fields:
                    if to_protect in data_name:
                        data_to_protect.add(data_name)
    
    additional_data_name = data_to_protect - data_to_unprotect
    removal_data_name = data_to_unprotect - data_to_protect
    data_name_already_protected = data_to_unprotect - removal_data_name - additional_data_name
    
    if not quiet:
        print "The following listing shows :"
        print " - Data which contain the substrings already defined in the current configuration"
        print " \033[32m- Data which contain the substrings you are adding\t => will be protected when the Synchronizer is restarted\033[0m"
        print " \033[31m- Data which contain the substrings you are removing\t => will be in cleartext when the Synchronizer is restarted\033[0m"
        print
    for data_name in sorted(data_name_already_protected):
        print          " Currently matching :                              %s" % data_name
    for data_name in sorted(additional_data_name):
        print "\033[%dm Added substrings ; matches :                      %s\033[0m" % (OK_COLOR, data_name)
    for data_name in sorted(removal_data_name):
        print "\033[%dm Removed substrings ; no substring match anymore : %s\033[0m" % (ERROR_COLOR, data_name)
    if not any(additional_data_name.union(removal_data_name)):
        if additional_substrings or removal_substrings:
            print "\n\033[33mThere is no field containing any of the substrings you are adding or removing.\033[0m\n"
    

def yn_choice(message, default='y'):
    choices = 'Y/n' if default.lower() in ('y', 'yes') else 'y/N'
    choice = raw_input("%s (%s) " % (message, choices))
    values = ('y', 'yes', '') if choices == 'Y/n' else ('y', 'yes')
    return choice.strip().lower() in values


def update_field_in_file(field, new_value, file_path):
    line_to_write = u"%s=%s\n" % (field, new_value)
    try:
        f = open(file_path, 'r')
        lines = f.read().splitlines()
        f.close()
    except Exception, exp:
        msg_error = 'Cannot change %s=%s into the file %s: %s' % (field, new_value, file_path, exp)
        if (exp.errno == errno.EPERM) or (exp.errno == errno.EACCES):
            msg_error = 'Cannot change %s:%s into the file %s : Permission denied' % (field, new_value, file_path)
        if exp.errno == errno.ENOENT:
            msg_error = 'Cannot change %s:%s into the file %s : File not found' % (field, new_value, file_path)
        return msg_error
    
    # Create temp file
    fh, abs_path = mkstemp()
    with fdopen(fh, 'w') as new_file:
        found = False
        for line in lines:
            # strip_line = line.strip()
            if line.startswith('#'):  # comments directly go to new_file
                new_file.write(line + '\n')
                continue
            
            elts = line.strip().split('=')
            if elts[0] != field:
                new_file.write(line + '\n')
                continue
            found = True
            # we have our field here, replace the line now
            new_file.write(line_to_write)
        if not found:
            # the field was not found, we must add the line
            new_file.write(line_to_write)
    # Move new file
    move(abs_path, file_path)
    return None


def main():
    try:
        subprocess.check_call(['shinken-daemons-has', 'synchronizer'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError:
        print "\n\n\033[31mThe Synchronizer is not installed on this server ; this tool is not relevant.\033[0m\n\n"
        sys.exit(1)
    
    parser = optparse.OptionParser()
    config_opts = optparse.OptionGroup(parser, 'General options')
    config_opts.add_option('-c', '--config', dest='synchronizer_config', action='store', type="string",
                           default="/etc/shinken/synchronizer.cfg", help="The synchronizer config cfg file from which the configuration is read")
    
    action_opts = optparse.OptionGroup(parser, 'Available actions')
    action_opts.add_option('-a', '--add', dest='additional_substrings', action='append', type="string",
                           default=[], help="Substrings you want to add in configuration", metavar="fields")
    action_opts.add_option('-r', '--remove', dest='removal_substrings', action='append', type="string",
                           default=[], help="Substrings you want to remove from configuration", metavar="fields")
    action_opts.add_option('-q', '--quiet', dest='quiet', action='store_true',
                           default=False, help="Only display meaningful information without all the detailed explanations", metavar="fields")
    
    parser.add_option_group(config_opts)
    parser.add_option_group(action_opts)
    
    opts, args = parser.parse_args()
    
    has_errors = False
    if len(args) > 0:
        print "\n\033[31m%s: error: Arguments %s are not allowed\033[0m\n" % (os.path.basename(sys.argv[0]), args)
        has_errors = True
    
    for field in opts.additional_substrings + opts.removal_substrings:
        if re.search(r'[^0-9a-zA-Z_-]', field):
            print "\n\033[31m%s: error: The substring [%s] contains forbidden characters\033[0m\n" % (os.path.basename(sys.argv[0]), field)
            has_errors = True
    
    if has_errors:
        parser.print_help()
        sys.exit(1)
    
    if not os.path.exists(opts.synchronizer_config):
        print "The synchronizer config file '%s' cannot be found" % opts.synchronizer_config
        sys.exit(2)
    
    synchronizer_config = get_conf([opts.synchronizer_config, SYNCHRONIZER_OVERLOAD_FILE])
    substrings_matching_fields_in_file = getattr(synchronizer_config, 'protect_fields__substrings_matching_fields')
    if isinstance(substrings_matching_fields_in_file, basestring):
        substrings_matching_fields_in_file = set(substrings_matching_fields_in_file.split(','))
    
    asked_to_add = set([f.upper() for f in opts.additional_substrings])
    asked_to_remove = set([f.upper() for f in opts.removal_substrings])
    new_substring_not_in_file = asked_to_add - set(substrings_matching_fields_in_file) - asked_to_remove
    
    bogus_fields = asked_to_add.intersection(asked_to_remove)
    if bogus_fields != set():
        print "\n\033[%dm You asked to add and to remove same fields : %s" % (ERROR_COLOR, ','.join([i for i in bogus_fields]))
        sys.exit(1)
    
    to_remove_in_file = asked_to_remove.intersection(substrings_matching_fields_in_file)
    not_remove_fields = asked_to_remove.difference(to_remove_in_file)
    if not opts.quiet:
        print "\nObjects data containing one of those substrings will be protected in Synchronizer interface. If encryption is enabled, these fields will be encrypt"
        print "\nSubstrings in current configuration file: ",
        print "%s\n" % " ".join(sorted(substrings_matching_fields_in_file))
        if new_substring_not_in_file:
            print "Substrings that will be \033[32madded\033[0m to the configuration : ",
            print "\033[32m%s\033[0m" % " ".join(sorted(new_substring_not_in_file))
        if to_remove_in_file:
            print "Substrings that will be \033[31mremoved\033[0m from the configuration : ",
            print "\033[31m%s\033[0m" % " ".join(sorted(to_remove_in_file))
        if not_remove_fields:
            print "Substrings that will not be \033[35mremoved\033[0m because they are not in the current configuration : ",
            print "\033[35m%s\033[0m" % " ".join(sorted(not_remove_fields))
        
        print
    
    new_substring_list = set(substrings_matching_fields_in_file.union(asked_to_add) - to_remove_in_file)
    new_substrings_to_write = ",".join(sorted(new_substring_list))
    
    if substrings_matching_fields_in_file != new_substring_list:
        print "Substrings which will be written in the new configuration : ",
        print "\033[33m%s\033[0m\n" % " ".join(sorted(new_substring_list))
    elif asked_to_remove or asked_to_add:
        print "You are not adding or removing any substring to the configuration.\n"
    
    # will show all the data impacted
    show_data_to_proceed(synchronizer_config, substrings_matching_fields_in_file, sorted(new_substring_not_in_file), sorted(to_remove_in_file), opts.quiet)
    if substrings_matching_fields_in_file != new_substring_list:
        accept_write = yn_choice("\nDo you want to save your new configuration in the file '%s'" % SYNCHRONIZER_OVERLOAD_FILE)
        if accept_write:
            msg_error = update_field_in_file("protect_fields__substrings_matching_fields", new_substrings_to_write, SYNCHRONIZER_OVERLOAD_FILE)
            if msg_error:
                print "\n'%s' save failed, %s" % (SYNCHRONIZER_OVERLOAD_FILE, msg_error)
                sys.exit(1)
            elif not opts.quiet:
                print "\n'%s' saved successfully" % SYNCHRONIZER_OVERLOAD_FILE
                print "\n\033[35mYou need to restart the synchronizer for the new configuration to take effect.\n\n\033[0m"
        else:
            print "\n Nothing was done."
    
    sys.exit(0)


if __name__ == '__main__':
    main()
