Skip to content
Snippets Groups Projects
migration 16.6 KiB
Newer Older
Matthieu Lamalle's avatar
Matthieu Lamalle committed
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Script to migrate configuration to new Zéphir
"""
import sys
from csv import DictWriter
from os.path import isfile, join, isdir
import argparse
from fnmatch import fnmatch
from glob import glob
from urllib3.exceptions import InsecureRequestWarning
import warnings
from collections import OrderedDict

from zephir_client.zephir_client import send_data
from zephir_client import ZcliConfig, configure_server, post_configure_server
from creole.loader import creole_loader
from tiramisu.config import SubConfig

warnings.simplefilter('ignore', InsecureRequestWarning)

if sys.version_info[0] < 3:
    from pyeole.encode import normalize
    input = raw_input
else:
    normalize = str
    unicode = str


class colored:
    NS = '\033[95m\033[1m\033[4m'
    FAMILY = '\033[94m\033[1m'
    TITLE = '\033[94m\033[1m'
    OK = '\033[92m'
    WARNING = '\033[93m'
    ERROR = '\033[91m'
    INFO = '\033[1m'
    CHOICE = '\033[1m'
    ENDC = '\033[0m'


class Migrate:
    def is_default_owner(self, option, index=None):
        return self.config.cfgimpl_get_values().is_default_owner(option, index=index)

    def display_variable(self, path, index, comment, value, prefix):
        csv = {'path': path,
               'index': index}
        if sys.version_info[0] < 3:
            csv['name'] = normalize(comment).encode('utf-8')
        else:
            csv['name'] = comment
        if index is None:
            tindex = 'null'
        else:
            tindex = str(index)
        if path in self.errors and tindex in self.errors[path]:
            color = colored.ERROR
            suffix = self.errors[path][tindex]
            status = 'error'
        elif path in self.warnings and tindex in self.warnings[path]:
            color = colored.WARNING
            suffix = self.warnings[path][tindex]
            status = 'warning'
        elif path in self.infos:
            color = colored.INFO
            suffix = self.infos[path]
            status = 'ok'
        else:
            color = colored.OK
            suffix = ''
            status = 'ok'
        if sys.version_info[0] < 3 and isinstance(value, unicode):
            csv['value'] = normalize(value).encode('utf-8')
        else:
            csv['value'] = value
        csv['status'] = status
        if sys.version_info[0] < 3:
            csv['message'] = normalize(suffix).encode('utf-8')
        else:
            csv['message'] = suffix
        if isinstance(value, list):
            value = (unicode(val) for val in value)
            value = u'{}, {}'.format(colored.ENDC,color).join(value)
        value = u'{}{}'.format(color,value)
        msg = u'{}{} : {}{}'.format(prefix, normalize(comment), value, colored.ENDC)
        if suffix:
            msg += u' - {}{}{}'.format(color, suffix, colored.ENDC)
        print(msg)
        if self.options.display_path:
            print('  {}({})'.format(prefix, path))
        self.csv.append(csv)

    def display_namespace(self, namespace):
        namespace = namespace.upper()
        print('\n{}{}{}'.format(colored.NS, namespace, colored.ENDC))

    def display_family(self, family, prefix):
        family = family.capitalize()
        print(u'{}{}{}{} :'.format(prefix, colored.FAMILY, normalize(family), colored.ENDC))

    def display_masterslaves(self, master_path, master_name, get_option, values):
        for index, value in enumerate(values[0]['values']):
            self.display_family(u'{} ({})'.format(normalize(master_name), index), '    ')
            self.display_variable(master_path, None, master_name, value, '        ')
            for slave_option in values[1:]:
                if index in slave_option['values']:
                    slave_path = slave_option['path']
                    name = get_option(slave_path).impl_get_display_name()
                    self.display_variable(slave_path,
                                          index,
                                          name,
                                          slave_option['values'][index]['value'],
                                          '        ')

    def display_report(self):
        old_namespace = ''
        old_family = ''
        get_option = self.config.cfgimpl_get_description().impl_get_opt_by_path
        for path, value in self.collect.items():
            namespace, family_name, option_path = path.split('.', 2)
            if namespace != old_namespace:
                self.display_namespace(namespace)
                family_prefix = '  '
                old_namespace = namespace
            else:
                family_prefix = '\n  '
            if family_name != old_family:
                display_name = get_option(namespace + '.' + family_name).impl_get_display_name()
                self.display_family(display_name, family_prefix)
            old_family = family_name
            name = get_option(path).impl_get_display_name()
            if '.' in option_path:
                # masterslaves
                self.display_masterslaves(path, name, get_option, value)
            else:
                # not a masterslaves
                self.display_variable(path, None, name, value['value'], '    ')

    def set_values(self):
        zcliconf = ZcliConfig()
        self.url = zcliconf.url
        self.version = zcliconf.version
        message = 'config.session.{}.start'.format(self.options.type)
        payload = {'id': self.options.id}
        try:
            result = send_data(message, self.url, self.version, payload)
        except Exception as err:
            print('impossible de démarrer une session de configuration {}'.format(err))
            sys.exit(1)
        self.session_id = result['sessionid']
        message = 'config.session.{}.filter'.format(self.options.type)
        payload = {'session_id': self.session_id, 'mode': 'expert'}
        try:
            result = send_data(message, self.url, self.version, payload)
        except Exception as err:
            print('impossible de filtrer la session de configuration {}'.format(err))
            sys.exit(1)
        try:
            ret = post_configure_server(self.url, self.session_id, self.send)
            self.errors = ret['errors']
            self.warnings = ret['warnings']
        except Exception as err:
            msg = str(err)
            if ':' in msg:
                msg = msg.split(': ', 1)[1]
            if msg == u'option "{}" inconnue dans l\'optiondescription "baseoption" (500)'.format(namespace):
                self.remote_config_error = "L'extra {} n'existe pas".format(namespace)
            else:
                print('Erreur : {}'.format(err))
                sys.exit(1)

    def is_exclude_or_include(self, path, exc_inc):
        if exc_inc:
            for ei in exc_inc:
                if fnmatch(path, ei):
                    return True
        return False

    def is_exclude(self, path):
        return self.is_exclude_or_include(path, self.options.exclude)

    def is_include(self, path):
        return self.is_exclude_or_include(path, self.options.include)

    def load_zephir(self):
        zephir_id = self.options.zephir_id
        dirs = glob('/var/lib/zephir/conf/*/{}'.format(zephir_id))
        if not dirs:
            raise Exception("impossible de trouver le serveur Zéphir avec l'ID {}".format(zephir_id))
        if len(dirs) != 1:
            raise Exception("Plusieurs servers ont l'ID {}".format(zephir_id))
        self.dict_dirs = [join(dirs[0], 'dicos/module'),
                          join(dirs[0], 'dicos/variante'),
                          join(dirs[0], 'dicos/local')]
        self.configeol = join(dirs[0], 'zephir.eol')
        extra_dir = join(dirs[0], 'fichiers_zephir/extra')
        self.extra_config = extra_dir
        if isdir(extra_dir):
            self.load_extra = True
        else:
            self.load_extra = False

    def load_error(self):
        setting = self.config.cfgimpl_get_settings()
        for path, properties in setting.get_modified_properties().items():
            if 'load_error' in properties:
                self.errors[path] = {None: "Problème de chargement de la variable localement"}

    def load_config(self):
        if self.is_zephir:
            self.load_zephir()
            # FIXME no extra on zephir !
            self.config = creole_loader(rw=True,
                                        load_extra=self.load_extra,
                                        reload_config=False,
                                        force_configeol=self.configeol,
                                        force_eoleextradico=self.extra_config,
                                        force_eoleextraconfig=self.extra_config,
                                        try_upgrade=False,
                                        force_dirs=self.dict_dirs)
        else:
            self.config = creole_loader(rw=True,
                                        load_extra=True)

    def populate_send(self, path, index, data, force):
        if self.is_exclude(path) or (self.options.only_include and not self.is_include(path)):
            self.infos[path] = 'La modification est exclue'
        else:
            self.send.append((path, index, data, force))

    def do_collect(self):
        self.collect = OrderedDict()
        self.send = []
        for namespace, subconfig in self.config.iter_groups():
            if namespace in ['containers', 'actions']:
                continue
            for family_name, family in subconfig.iter_all():
                for variable_name, variable in family.iter_all():
                    if isinstance(variable, SubConfig):
                        ## it's a master/slaves
                        try:
                            masterslaves = iter(variable.iter_all())
                            master_name, master_values = next(masterslaves)
                            path = namespace + '.' + family_name + '.' + variable_name + '.' + master_name
                            migrate_same = self.options.migrate_same or self.is_include(path)
                            option = self.config.cfgimpl_get_description().impl_get_opt_by_path(path)
                            if not self.is_default_owner(option):
                                master_len = len(master_values)
                                self.collect[path] = [{'values': master_values, 'slaves': {}}]
                                self.populate_send(path, None, master_values, migrate_same)
                                for slave_name, slave_values in masterslaves:
                                    slave_path = namespace + '.' + family_name + '.' + variable_name + '.' + slave_name
                                    collect_slave = {'path': slave_path, 'values': {}}
                                    slave_option = self.config.cfgimpl_get_description().impl_get_opt_by_path(slave_path)
                                    for index in range(master_len):
                                        if not self.is_default_owner(slave_option, index):
                                            slave_migrate_same = self.options.migrate_same or self.is_include(slave_path)
                                            self.populate_send(slave_path, index, slave_values[index], slave_migrate_same)
                                            collect_slave['values'][index] = {'value': slave_values[index]}
                                    self.collect[path].append(collect_slave)
                        except StopIteration:
                            pass
                    else:
                        # it's an option
                        path = namespace + '.' + family_name + '.' + variable_name
                        migrate_same = self.options.migrate_same or self.is_include(path)
                        option = self.config.cfgimpl_get_description().impl_get_opt_by_path(path)
                        if path in self.errors or not self.is_default_owner(option):
                            self.collect[path] = {'value': variable}
                            if not self.is_default_owner(option):
                                self.populate_send(path, None, variable, migrate_same)

    def make_csv_name(self):
        index = 0
        self.csv_filename = 'migrate_{}.csv'.format(index)
        while isfile(self.csv_filename):
            index += 1
            self.csv_filename = 'migrate_{}.csv'.format(index)

    def write_csv(self):
        with open(self.csv_filename, 'w') as csvfile:
            fieldnames = list(self.csv[0].keys())
            fieldnames.sort()
            writer = DictWriter(csvfile, fieldnames=fieldnames)
            writer.writeheader()
            for csv in self.csv:
                writer.writerow(csv)

    def finalise(self):
        type_ = {'server': 'serveur',
                 'servermodel': 'modèle de serveur'}[self.options.type]
        print('{}Voulez-vous sauvegarder la configuration pour ce {} ?{}'.format(colored.TITLE, type_, colored.ENDC))
        print('{}1{}: oui'.format(colored.CHOICE, colored.ENDC))
        print("{}2{}: non et annuler la session d'édition".format(colored.CHOICE, colored.ENDC))
        print("{}3{}: non et conserver la session d'édition".format(colored.CHOICE, colored.ENDC))
        resp = None
        while resp not in ['1', '2', '3']:
            if resp is not None:
                print('{}Valeur incorrecte{}'.format(colored.ERROR, colored.ENDC))
            resp = input('-> ')
        if resp in ['1', '2']:
            message = 'config.session.{}.stop'.format(self.options.type)
            payload = {'sessionid': self.session_id,
                       'save': resp == '1'}
            try:
                result = send_data(message, self.url, self.version, payload)
            except Exception as err:
                print('impossible de terminer une session de configuration {}'.format(err))
                sys.exit(1)
        else:
            print("L'ID de la session de configuration courante est {}".format(self.session_id))

    def __init__(self, options, is_zephir):
        self.options = options
        self.session_id = None
        self.errors = {}
        self.warnings = {}
        self.infos = {}
        self.remote_config = None
        self.csv = []
        self.make_csv_name()
        self.is_zephir = is_zephir

        print('{}Chargement de la configuration locale{}'.format(colored.TITLE, colored.ENDC))
        self.load_config()
        self.load_error()
        self.do_collect()
        print('{}Chargement de la configuration distante{}'.format(colored.TITLE, colored.ENDC))
        self.set_values()
        print('{}Récapitulatif de la configuration{}'.format(colored.TITLE, colored.ENDC))
        self.display_report()
        if self.csv:
            print('{}Écriture du rapport dans le fichier {}{}'.format(colored.TITLE, self.csv_filename, colored.ENDC))
            self.write_csv()
        self.finalise()

if __name__ == "__main__":
    config = creole_loader()
    is_zephir = config.creole.general.eole_module == 'zephir'
    parser = argparse.ArgumentParser()
    parser.add_argument('-t',
                        '--type',
                        help='Type de session a créer',
                        choices=['server', 'servermodel'],
                        default='server')
    if is_zephir:
        parser.add_argument('-z',
                            '--zephir_id',
                            help="ID du serveur sur le Zéphir",
                            required=True,
                            type=int)
    parser.add_argument('-i',
                        '--id',
                        help="ID du serveur ou du modèle de serveur",
                        required=True,
                        type=int)
    parser.add_argument('-p',
                        '--path',
                        help='Afficher les chemins en dessous des variables',
                        action='store_true',
                        dest='display_path')
    parser.add_argument('-s',
                        '--same',
                        help='Enregistrer les variables même si cela correspond à la valeur actuelle de la variable',
                        action='store_true',
                        dest='migrate_same')
    parser.add_argument('-e',
                        '--exclude',
                        nargs='*',
                        help='Liste des chemins de variable à exclure dans la migration (Unix shell-style wildcards)')
    parser.add_argument('-n',
                        '--include',
                        nargs='*',
                        help='Liste des chemins de variable à inclure dans la migration (Unix shell-style wildcards)')
    parser.add_argument('-o',
                        '--only-include',
                        help='Ne migrer que les variables inluses',
                        action='store_true')

    options = parser.parse_args()
    Migrate(options, is_zephir)