#! /usr/bin/python3
#
# Copyright (c) 2018 Ultimum Technologies s.r.o.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Author: Michal Arbet <michal.arbet@ultimum.io>

from stevedore.named import ExtensionManager
from oslo_config import cfg
import argparse
import os
import fileinput
import re
import logging
import sys
import subprocess

# Logging
root = logging.getLogger()
root.setLevel(logging.INFO)

LOG = logging.getLogger("neutron-plugin-manage")
LOG.setLevel(logging.INFO)
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log_handler.setFormatter(formatter)
root.addHandler(log_handler)


class MyExtensionManager(ExtensionManager):
    def __init__(self, namespace,
                 invoke_on_load=False,
                 invoke_args=(),
                 invoke_kwds={},
                 propagate_map_exceptions=False,
                 on_load_failure_callback=None,
                 verify_requirements=False):
        super(MyExtensionManager,
              self).__init__(namespace,
                             invoke_on_load=invoke_on_load,
                             invoke_args=invoke_args,
                             invoke_kwds=invoke_kwds,
                             propagate_map_exceptions=propagate_map_exceptions,
                             on_load_failure_callback=on_load_failure_callback,
                             verify_requirements=verify_requirements)

    def _load_plugins(self, invoke_on_load, invoke_args, invoke_kwds,
                      verify_requirements):
        extensions = []
        for ep in self.list_entry_points():
            if not ep.name.startswith('neutron'):
                continue
            LOG.debug('found extension %r', ep)
            try:
                ext = self._load_one_plugin(ep,
                                            invoke_on_load,
                                            invoke_args,
                                            invoke_kwds,
                                            verify_requirements,
                                            )
                if ext:
                    extensions.append(ext)
            except (KeyboardInterrupt, AssertionError):
                raise
            except Exception as err:
                if self._on_load_failure_callback is not None:
                    self._on_load_failure_callback(self, ep, err)
                else:
                    # Log the reason we couldn't import the module,
                    # usually without a traceback. The most common
                    # reason is an ImportError due to a missing
                    # dependency, and the error message should be
                    # enough to debug that.  If debug logging is
                    # enabled for our logger, provide the full
                    # traceback.
                    LOG.error('Could not load %r: %s', ep.name, err,
                              exc_info=LOG.isEnabledFor(logging.DEBUG))
        return extensions


class NeutronArgsParser(object):
    def __init__(self, neutron_plugin_manager):
        self.available_service_plugins = list(
            neutron_plugin_manager.available_service_plugins.keys())
        self.available_l3_extensions = list(
            neutron_plugin_manager.available_l3_extensions.keys())
        self.neutron_plugin_manager = neutron_plugin_manager
        self._init_parser()

    def _enable(self, args):
        """
        Enable service_plugin/l3_extension, and also create symlinks to
        loadable neutron's config path.
        :param args:
        :return: None
        """
        if args.service_plugin:
            self.neutron_plugin_manager.enable(args.service_plugin,
                                               'service_plugin')
        if args.l3_extension:
            self.neutron_plugin_manager.enable(args.l3_extension,
                                               'l3_extension')
        self.neutron_plugin_manager.restart_services()

    def _disable(self, args):
        """
        Disable service_plugin/l3_extension, and also remove symlinks from
        loadable neutron's config path.
        :param args:
        :return: None
        """
        if args.service_plugin:
            self.neutron_plugin_manager.disable(args.service_plugin,
                                                'service_plugin')
        if args.l3_extension:
            self.neutron_plugin_manager.disable(args.l3_extension,
                                                'l3_extension')
        self.neutron_plugin_manager.restart_services()

    def _init_parser(self):
        """
        Set argsparse options and their allowed arguments
        taken from NeutronPluginManager object.
        :return: None
        """
        self.parser = argparse.ArgumentParser(prog='neutron-plugin-manage',
                                              add_help=False)
        self.parser.add_argument('--service-plugin', action='store',
                                 dest='service_plugin',
                                 help='Define service_plugin.',
                                 choices=self.available_service_plugins)
        self.parser.add_argument('--l3-extension', action='store',
                                 dest='l3_extension',
                                 help='Define l3_extension.',
                                 choices=self.available_l3_extensions)

        sp = self.parser.add_subparsers()
        sp_enable = sp.add_parser('enable', help='Enable.',
                                  parents=[self.parser])
        sp_disable = sp.add_parser('disable', parents=[self.parser],
                                   help='Disable.')
        sp_enable.set_defaults(func=self._enable)
        sp_disable.set_defaults(func=self._disable)

    def parse(self):
        """
        Parse options and arguments from command line.
        :return: None
        """
        args = self.parser.parse_args()
        args.func(args)


class NeutronPluginManager(object):

    def __init__(self, configs):
        self.configs = configs
        self.available_service_plugins = self._stevedore_discover(
            'neutron.service_plugins')
        self.available_l3_extensions = self._stevedore_discover(
            'neutron.agent.l3.extensions')
        self.CONF = self._load_neutron_config()
        self.restart_trigger = []
        self.neutron_config_dir = "/etc/neutron/"
        self.neutron_server_config_dir = os.path.join(self.neutron_config_dir,
                                                      "server.conf.d/")
        self.neutron_agent_config_dir = os.path.join(self.neutron_config_dir,
                                                     "agent.conf.d/")

    @property
    def restart_needed(self):
        """
        Property of object which is telling if restart is needed or not.
        :rtype: bool
        """
        return True in self.restart_trigger

    def restart_services(self):
        """
        Restart neutron-* services.
        :return: None
        """
        if self.restart_needed:
            LOG.info("Restarting neutron services.")
            os.system("sudo systemctl restart neutron-*.service")
        else:
            LOG.info("Restart is not needed.")

    def _request_restart(self, restart=True):
        """
        Request restart. This function is typically called when configs are
        changed or symlinks created/removed.
        :param restart:
        :return: None
        """
        self.restart_trigger.append(restart)

    @staticmethod
    def _stevedore_discover(namespace):
        """
        Discover all available entrypoints from some defined namespace.
        Example namespace: neutron.l3.extensions
        :param namespace:
        :return: elements:
        :rtype: dict
        """
        stevedore_elements = ExtensionManager(namespace=namespace,
                                              invoke_on_load=False)
        elements = {}
        for name, entry_point in stevedore_elements.items():
            try:
                element = dict()
                element['name'] = name
                element['module'] = entry_point.entry_point_target.split(".")[
                    0]
                element['target'] = entry_point.entry_point_target
                elements.update({name: element})
            except Exception as e:
                LOG.warning('Stevedore discover failed for {}'.format(name),
                            exc_info=e)
        return elements

    def _load_neutron_config(self):
        """
        Load neutron configuration via oslo.cfg
        :return: None
        """
        if os.getuid() == 0:
            oslo_config_opts = MyExtensionManager(namespace='oslo.config.opts',
                                                  invoke_on_load=True)
            for k, v in oslo_config_opts.items():
                for group, items in v.obj:
                    cfg.CONF.register_opts(items, group)
            args_oslo = ["--config-file=" + i for i in self.configs]
            cfg.CONF(args=args_oslo)
            return cfg.CONF
        else:
            LOG.error("This username is not in the sudoers file. Sorry :(")
            sys.exit(9)

    def _neutron_module(self, element, element_type):
        """
        Returns module name of some element with specified type.
        Could be service_plugin/l3_extension.
        :rtype: str
        :param element:
        :param element_type:
        :return: neutron_module:
        """
        if element_type == 'service_plugin':
            return self.available_service_plugins.get(element).get('module')
        elif element_type == 'l3_extension':
            return self.available_l3_extensions.get(element).get('module')

    def _elements_enabled(self, element_type):
        """
        Returns list of elements enabled for specified type of element.
        Could be service_plugin/l3_extension
        :rtype: list
        :param element_type: str
        :return: elements
        """
        if element_type == 'service_plugin':
            return self.CONF.service_plugins
        elif element_type == 'l3_extension':
            return self.CONF.agent.extensions

    def _available_elements(self, element_type):
        """
        Returns a list of available elements for specified type
        which can be set in neutron. Could be service_plugin/l3_extension.
        :rtype: dict
        :param element_type: str
        :return: elements:
        """
        if element_type == 'service_plugin':
            return self.available_service_plugins
        elif element_type == 'l3_extension':
            return self.available_l3_extensions

    def _element(self, name, element_type):
        """
        Returns an element.
        :rtype: dict
        :param name:
        :param element_type:
        :return: elements
        """
        return self._available_elements(element_type).get(name)

    @staticmethod
    def _replace(filename, search_exp, replace_exp):
        """
        Replace line inplace.
        :param filename:
        :param search_exp:
        :param replace_exp:
        :return: None
        """
        uid = os.stat(filename).st_uid
        gid = os.stat(filename).st_gid
        config = fileinput.FileInput(filename, inplace=True)
        for line in config:
            line = re.sub(search_exp, replace_exp, line.rstrip())
            print(line)
        os.chown(filename, uid, gid)

    def _add_to_config(self, name, element_type):
        """
        Add element to configuration file depends on element type.
        Service_plugin -> conf : /etc/neutron/neutron.conf
        L3_extension   -> conf : /etc/neutron/l3_agent.ini
        :param name:
        :param element_type:
        :return: None
        """
        elements_enabled = self._elements_enabled(element_type)
        elements_enabled.append(name)
        filename = search_exp = replace_exp = None
        if element_type == 'service_plugin':
            filename = self.configs[0]
            search_exp = '^#?service_plugins.*'
            replace_exp = 'service_plugins = {}'.format(
                ",".join(elements_enabled))
        elif element_type == 'l3_extension':
            filename = self.configs[1]
            search_exp = '^#?extensions.*'
            replace_exp = 'extensions = {}'.format(",".join(elements_enabled))
        self._replace(filename, search_exp, replace_exp)
        self._request_restart()

    def _remove_from_config(self, name, element_type):
        """
        Remove element from configuration file depends on element type.
        Service_plugin -> conf : /etc/neutron/neutron.conf
        L3_extension   -> conf : /etc/neutron/l3_agent.ini
        :param name:
        :param element_type:
        :return: None
        """
        elements_enabled = self._elements_enabled(element_type)
        elements_enabled.remove(name)
        filename = search_exp = replace_exp = None
        if element_type == 'service_plugin':
            filename = self.configs[0]
            search_exp = '^#?service_plugins.*'
            replace_exp = 'service_plugins = {}'.format(
                ",".join(elements_enabled))
        elif element_type == 'l3_extension':
            filename = self.configs[1]
            search_exp = '^#?extensions.*'
            replace_exp = 'extensions = {}'.format(",".join(elements_enabled))
        self._replace(filename, search_exp, replace_exp)
        self._request_restart()

    def _check_if_enabled(self, name, element_type):
        """
        Checks if specific type of element is enabled.
        :rtype: bool
        :param name:
        :param element_type:
        :return: enabled:
        """
        elements_enabled = self._elements_enabled(element_type)
        element = self._element(name, element_type)
        return element.get('name') in elements_enabled

    def _check_if_enabled_another(self, name, element_type):
        """
        This function checks if specific type of element is enabled, or
        if some other element from same neutron's module is enabled already.
        :rtype: bool
        :param name:
        :param element_type:
        :return: enabled
        """
        neutron_module = self._neutron_module(name, element_type)
        elements_enabled = self._elements_enabled(element_type)
        for i in elements_enabled:
            if self._neutron_module(i, element_type) == neutron_module:
                return True
        return False

    def enable(self, name, element_type):
        """
        Enable element in neutron config and calls function
        for link configuration files into loadable path.
        :param name:
        :param element_type:
        :return: None
        """
        neutron_module = self._neutron_module(name, element_type)
        # If neutron_module is not neutron, that means
        # we can have only one {service_plugin, l3_extension}
        # enabled at once.
        #
        # Example for neutron_fwaas:
        # firewall OR firewall_v2
        if neutron_module != 'neutron':
            # Check if already not enabled
            if not self._check_if_enabled(name, element_type):
                # If no, check also if some other is not enabled from
                # this neutron_module
                if not self._check_if_enabled_another(name, element_type):
                    LOG.info("Enabling {} {}.".format(element_type, name))
                    self._add_to_config(name, element_type)
                else:
                    LOG.warning(
                        "Enabling {} failed. Another {} from {} is currently "
                        "enabled. If you want to enable {} , firstly disable "
                        "currently enabled {}.".format(
                            element_type, element_type,
                            self._neutron_module(name, element_type), name,
                            element_type))
            else:
                LOG.warning(
                    "Enabling {} {} failed. This {} "
                    "is already enabled.".format(element_type, name,
                                                 element_type))
        # If neutron module is neutron, that means we can enable
        # multiple {service_plugins, l3_extensions}.
        #
        # Example: router,metering,qos )
        else:
            if not self._check_if_enabled(name, element_type):
                LOG.info("Enabling {} {}.".format(element_type, name))
                self._add_to_config(name, element_type)
            else:
                LOG.warning(
                    "Enabling {} {} failed. This {} "
                    "is already enabled.".format(element_type, name,
                                                 element_type))

    def disable(self, element, element_type):
        """
        Disable element in neutron config and calls function
        for unlink configuration files from loadable path.
        :param element:
        :param element_type:
        :return: None
        """
        if self._check_if_enabled(element, element_type):
            LOG.info("Disabling {} {}".format(element_type, element))
            self._remove_from_config(element, element_type)
        else:
            LOG.warning(
                "Disabling {} {} failed, this {} "
                "is not currently enabled.".format(element_type, element,
                                                   element_type))

def main():
    manager = NeutronPluginManager(
        ['/etc/neutron/neutron.conf', '/etc/neutron/l3_agent.ini'])
    parser = NeutronArgsParser(manager)
    parser.parse()


if __name__ == "__main__":
    main()

