<reponame>openstack/charm-interface-neutron-plugin-api-subordinate<gh_stars>1-10
import uuid
import json

import charms.reactive as reactive

from charms.reactive import hook
from charms.reactive import RelationBase
from charms.reactive import scopes


class NeutronPluginAPISubordinate(RelationBase):
    scope = scopes.GLOBAL

    @hook(
        '{provides:neutron-plugin-api-subordinate}-relation-{joined,changed}')
    def changed(self):
        """Set connected state and assess available state"""
        self.set_state('{relation_name}.connected')
        if self.neutron_api_ready() and not self.db_migration_pending():
            self.set_state('{relation_name}.available')

    @hook(
        '{provides:neutron-plugin-api-subordinate}-relation-{broken,departed}')
    def broken(self):
        """Remove connected state"""
        self.remove_state('{relation_name}.available')
        self.remove_state('{relation_name}.connected')

    @property
    def neutron_config_data(self):
        """Retrive and decode ``neutron_config_data`` from relation"""
        return json.loads(self.get_remote('neutron_config_data', "{}"))

    def neutron_api_ready(self):
        """Assess remote readiness"""
        if self.get_remote('neutron-api-ready') == 'yes':
            return True
        return False

    def configure_plugin(self, neutron_plugin, core_plugin=None,
                         neutron_plugin_config=None, service_plugins=None,
                         subordinate_configuration=None,
                         extension_drivers=None, mechanism_drivers=None,
                         tenant_network_types=None,
                         neutron_security_groups=None):
        """Send principle plugin information

        :param neutron_plugin: Neutron plugin name
        :type neutron_plugin: str
        :param core_plugin: Entry point eg neutron.plugins.ml2.plugin.Ml2Plugin
        :type core_plugin: str
        :param neutron-plugin-config: /etc/neutron/plugins/ml2/ml2_conf.ini
        :type neutron-plugin-config: str
        :param service-plugins: Comma delimited list of service plugins eg
                                router,firewall,lbaas,vpnaas,metering
        :type service-plugins: str
        :param subordinate_configuration: Configuration for the principle
                                          to inject into a configuration
                                          file it is managing eg:
            # Add sections and tuples to insert values into neutron-server's
            # neutron.conf e.g.
            # {
            #     "neutron-api": {
            #        "/etc/neutron/neutron.conf": {
            #             "sections": {
            #                 'DEFAULT': [
            #                     ('key1', 'val1')
            #                     ('key2', 'val2')
            #                 ],
            #                 'agent': [
            #                     ('key3', 'val3')
            #                 ],
            #             }
            #         }
            #     }
            # }
        :type subordinate_configuration:
            Dict[str, Dict[
                str Dict[
                    str, List[
                        Union[str, str]]]]]
        :param extension_drivers: Extension drivers eg dns,port_security
        :type extension_drivers: str
        :param mechanism_drivers: Mechanism drivers eg openvswitch,hyperv
        :type mechanism_drivers: str
        :param tenant_network_types: Tenant network types eg local,gre
                                     Note that this also configures
                                     ``type_drivers``
        :type tenant_network_types: str
        :param neutron_security_groups: 'true' to enable security groups
        :type neutron_security_groups: str
        """
        if subordinate_configuration is None:
            subordinate_configuration = {}
        relation_info = {
            'neutron-plugin': neutron_plugin,
            'core-plugin': core_plugin,
            'neutron-plugin-config': neutron_plugin_config,
            'service-plugins': service_plugins,
            'extension-drivers': extension_drivers,
            'mechanism-drivers': mechanism_drivers,
            'tenant-network-types': tenant_network_types,
            'neutron-security-groups': neutron_security_groups,
            'subordinate_configuration': json.dumps(subordinate_configuration),
        }
        self.set_remote(**relation_info)

    def request_restart(self, service_type=None):
        """Request a restart of a set of remote services

        :param service_type: string Service types to be restarted eg 'neutron'.
                                    If ommitted a request to restart all
                                    services is sent
        """
        if service_type:
            key = 'restart-trigger-{}'.format(service_type)
        else:
            key = 'restart-trigger'
        relation_info = {
            key: str(uuid.uuid4()),
        }
        self.set_remote(**relation_info)

    def request_db_migration(self):
        """Request principal to perform a DB migration"""
        if not self.neutron_api_ready():
            # Ignore the charm request until we are in a relation-changed hook
            # where the prinicpal charm has declared itself ready.
            return
        nonce = str(uuid.uuid4())
        relation_info = {
            'migrate-database-nonce': nonce,
        }
        self.set_remote(**relation_info)
        # NOTE: we use flags instead of RelationBase state here both because of
        #       easier interaction with charm code, and because of how states
        #       interact with RelationBase conversations leading to crashes
        #       when used prior to relation being fully established.
        reactive.set_flag('{relation_name}.db_migration'
                          .format(relation_name=self.relation_name))
        reactive.set_flag('{relation_name}.db_migration.'
                          .format(relation_name=self.relation_name)+nonce)
        reactive.clear_flag('{relation_name}.available'
                            .format(relation_name=self.relation_name))

    def db_migration_pending(self):
        """Assess presence and state of optional DB migration request"""
        # NOTE: we use flags instead of RelationBase state here both because of
        #       easier interaction with charm code, and because of how states
        #       interact with RelationBase conversations leading to crashes
        #       when used prior to relation being fully established.
        flag_prefix = ('{relation_name}.db_migration'
                       .format(relation_name=self.relation_name))
        if not reactive.is_flag_set(flag_prefix):
            return False
        flag_nonce = '.'.join(
            (flag_prefix,
             self.get_remote('migrate-database-nonce', '')))
        if reactive.is_flag_set(flag_nonce):
            # hooks fire in a nondeterministic order, and there will be
            # occations where a different hook run between the
            # ``migrate-database-nonce`` being set and it being returned to us
            # a subsequent relation-changed hook.
            #
            # to avoid buildup of unreaped db_migration nonce flags we remove
            # all of them each time we have a match for one.
            for flag in reactive.get_flags():
                if flag.startswith(flag_prefix):
                    reactive.clear_flag(flag)
            return False
        return True
