#!/usr/bin/env python3
#
# Copyright 2016-2021 Canonical Ltd
#
# 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.

import json
import os
import sys

_path = os.path.dirname(os.path.realpath(__file__))
_root = os.path.abspath(os.path.join(_path, '..'))


def _add_path(path):
    if path not in sys.path:
        sys.path.insert(1, path)


_add_path(_root)

from subprocess import check_call

from charmhelpers.core import unitdata

from charmhelpers.core.hookenv import (
    Hooks,
    UnregisteredHookError,
    config,
    log,
    DEBUG,
    INFO,
    WARNING,
    relation_get,
    relation_ids,
    relation_set,
    related_units,
    status_set,
    open_port,
    is_leader,
    relation_id,
    leader_set,
)

from charmhelpers.core.host import (
    service_pause,
    service_stop,
    service_start,
    service_restart,
)

from charmhelpers.fetch import (
    apt_install, apt_update,
    filter_installed_packages
)

from charmhelpers.contrib.openstack.utils import (
    configure_installation_source,
    openstack_upgrade_available,
    sync_db_with_multi_ipv6_addresses,
    os_release,
    pausable_restart_on_change as restart_on_change,
    is_unit_paused_set,
    CompareOpenStackReleases,
    snap_install_requested,
    install_os_snaps,
    get_snaps_install_info_from_origin,
    enable_memcache,
    series_upgrade_prepare,
    series_upgrade_complete,
    inform_peers_if_ready,
    check_api_unit_ready,
    check_api_application_ready,
    is_db_ready,
)

from keystone_context import fernet_enabled

from keystone_utils import (
    add_service_to_keystone,
    bootstrap_keystone,
    ensure_all_service_accounts_protected_for_pci_dss_options,
    add_credentials_to_keystone,
    determine_packages,
    disable_unused_apache_sites,
    do_openstack_upgrade_reexec,
    ensure_initial_admin,
    get_admin_passwd,
    migrate_database,
    save_script_rc,
    post_snap_install,
    register_configs,
    restart_map,
    services,
    CLUSTER_RES,
    KEYSTONE_CONF,
    POLICY_JSON,
    TOKEN_FLUSH_CRON_FILE,
    setup_ipv6,
    send_notifications,
    is_db_initialised,
    is_service_present,
    delete_service_entry,
    assess_status,
    run_in_apache,
    restart_function_map,
    WSGI_KEYSTONE_API_CONF,
    restart_pid_check,
    get_api_version,
    ADMIN_DOMAIN,
    ADMIN_PROJECT,
    create_or_show_domain,
    restart_keystone,
    key_leader_set,
    key_setup,
    key_write,
    pause_unit_helper,
    resume_unit_helper,
    remove_old_packages,
    stop_manager_instance,
    assemble_endpoints,
    endpoints_dict,
    endpoints_checksum,
)

from charmhelpers.contrib.hahelpers.cluster import (
    is_elected_leader,
    https,
    is_clustered,
)

from charmhelpers.contrib.openstack.ha.utils import (
    generate_ha_relation_data,
    expect_ha,
)

from charmhelpers.payload.execd import execd_preinstall
from charmhelpers.contrib.peerstorage import (
    peer_echo,
)
from charmhelpers.contrib.openstack.ip import (
    ADMIN,
    PUBLIC,
    resolve_address,
)

from charmhelpers.contrib.network.ip import (
    get_relation_ip,
)
from charmhelpers.contrib.openstack.context import ADDRESS_TYPES

from charmhelpers.contrib.charmsupport import nrpe

from charmhelpers.contrib.hardening.harden import harden

from charmhelpers.contrib.openstack.cert_utils import (
    get_certificate_request,
    process_certificates,
)

from charmhelpers.contrib.openstack.policyd import (
    maybe_do_policyd_overrides,
    maybe_do_policyd_overrides_on_config_changed,
)


hooks = Hooks()
CONFIGS = register_configs()


@hooks.hook('install.real')
@harden()
def install():
    status_set('maintenance', 'Executing pre-install')
    execd_preinstall()
    configure_installation_source(config('openstack-origin'))
    status_set('maintenance', 'Installing apt packages')
    apt_update()
    apt_install(determine_packages(), fatal=True)

    if snap_install_requested():
        status_set('maintenance', 'Installing keystone snap')
        # NOTE(thedac) Setting devmode until LP#1719636 is fixed
        install_os_snaps(
            get_snaps_install_info_from_origin(
                ['keystone'],
                config('openstack-origin'),
                mode='devmode'))
        post_snap_install()
        service_stop('snap.keystone.*')
    else:
        # unconfigured keystone service will prevent start of haproxy in some
        # circumstances. make sure haproxy runs. LP #1648396
        service_stop('keystone')
        service_start('haproxy')
        if run_in_apache():
            disable_unused_apache_sites()
            service_pause('keystone')
    # call the policy overrides handler which will install any policy overrides
    maybe_do_policyd_overrides(
        os_release('keystone'),
        'keystone',
        restart_handler=lambda: service_restart('apache2'))


@hooks.hook('config-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@harden()
def config_changed():
    # if we are paused, delay doing any config changed hooks.
    # It is forced on the resume.
    if is_unit_paused_set():
        log("Unit is pause or upgrading. Skipping config_changed", "WARN")
        return

    if config('prefer-ipv6'):
        status_set('maintenance', 'configuring ipv6')
        setup_ipv6()
        sync_db_with_multi_ipv6_addresses(config('database'),
                                          config('database-user'))

    if not config('action-managed-upgrade'):
        if openstack_upgrade_available('keystone'):
            status_set('maintenance', 'Running openstack upgrade')
            do_openstack_upgrade_reexec(configs=CONFIGS)

    for r_id in relation_ids('cluster'):
        cluster_joined(rid=r_id)

    # call the policy overrides handler which will install any policy overrides
    maybe_do_policyd_overrides_on_config_changed(
        os_release('keystone'),
        'keystone',
        restart_handler=lambda: service_restart('apache2'))

    config_changed_postupgrade()


@hooks.hook('config-changed-postupgrade')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@harden()
def config_changed_postupgrade():
    save_script_rc()
    release = os_release('keystone')
    if run_in_apache(release=release):
        # Need to ensure mod_wsgi is installed and apache2 is reloaded
        # immediately as charm queries its local keystone before restart
        # decorator can fire
        apt_install(filter_installed_packages(determine_packages()))
        # when deployed from source, init scripts aren't installed
        service_pause('keystone')

        disable_unused_apache_sites()
        if WSGI_KEYSTONE_API_CONF in CONFIGS.templates:
            CONFIGS.write(WSGI_KEYSTONE_API_CONF)
        if not is_unit_paused_set():
            restart_pid_check('apache2')
            stop_manager_instance()

    if enable_memcache(release=release):
        # If charm or OpenStack have been upgraded then the list of required
        # packages may have changed so ensure they are installed.
        apt_install(filter_installed_packages(determine_packages()))

    if is_leader() and fernet_enabled():
        key_setup()
        key_leader_set()

    configure_https()
    open_port(config('service-port'))

    update_nrpe_config()

    CONFIGS.write_all()

    if snap_install_requested() and not is_unit_paused_set():
        service_restart('snap.keystone.*')
        stop_manager_instance()

    if (is_db_initialised() and is_elected_leader(CLUSTER_RES) and not
            is_unit_paused_set()):
        ensure_initial_admin(config)
        if CompareOpenStackReleases(
                os_release('keystone')) >= 'liberty':
            CONFIGS.write(POLICY_JSON)

    update_all_identity_relation_units()
    update_all_domain_backends()
    update_all_fid_backends()

    for r_id in relation_ids('ha'):
        ha_joined(relation_id=r_id)

    notify_middleware_with_release_version()
    inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('shared-db-relation-joined')
def db_joined():
    if config('prefer-ipv6'):
        sync_db_with_multi_ipv6_addresses(config('database'),
                                          config('database-user'))
    else:
        # Avoid churn check for access-network early
        access_network = None
        for unit in related_units():
            access_network = relation_get(unit=unit,
                                          attribute='access-network')
            if access_network:
                break
        host = get_relation_ip('shared-db', cidr_network=access_network)

        relation_set(database=config('database'),
                     username=config('database-user'),
                     hostname=host)


def update_all_identity_relation_units():
    unit_ready, _ = check_api_application_ready()
    if not unit_ready:
        log(
            ("Keystone charm unit not ready - deferring identity-relation "
             "updates"),
            level=INFO)
        return

    log('Firing identity_changed hook for all related services.')
    for rid in relation_ids('identity-service'):
        for unit in related_units(rid):
            identity_changed(relation_id=rid, remote_unit=unit)
    log('Firing admin_relation_changed hook for all related services.')
    for rid in relation_ids('identity-admin'):
        admin_relation_changed(rid)
    log('Firing identity_credentials_changed hook for all related services.')
    for rid in relation_ids('identity-credentials'):
        for unit in related_units(rid):
            identity_credentials_changed(relation_id=rid, remote_unit=unit)


def update_all_domain_backends():
    """Re-trigger hooks for all domain-backend relations/units"""
    for rid in relation_ids('domain-backend'):
        for unit in related_units(rid):
            domain_backend_changed(relation_id=rid, unit=unit)


def update_all_fid_backends():
    if CompareOpenStackReleases(os_release('keystone')) < 'ocata':
        log('Ignoring keystone-fid-service-provider relation as it is'
            ' not supported on releases older than Ocata')
        return
    """If there are any config changes, e.g. for domain or service port
    make sure to update those for all relation-level buckets"""
    for rid in relation_ids('keystone-fid-service-provider'):
        update_keystone_fid_service_provider(relation_id=rid)


@restart_on_change(restart_map(), restart_functions=restart_function_map())
def leader_init_db_if_ready(use_current_context=False):
    """ Initialise the keystone db if it is ready and mark it as initialised.

    NOTE: this must be idempotent.
    """
    if not is_elected_leader(CLUSTER_RES):
        log("Not leader - skipping db init", level=DEBUG)
        return

    if is_db_initialised():
        log("Database already initialised - skipping db init", level=DEBUG)
        update_all_identity_relation_units()
        return

    # Bugs 1353135 & 1187508. Dbs can appear to be ready before the
    # units acl entry has been added. So, if the db supports passing
    # a list of permitted units then check if we're in the list.
    if not is_db_ready(use_current_context=use_current_context):
        log('Allowed_units list provided and this unit not present',
            level=INFO)
        return

    migrate_database()
    bootstrap_keystone(configs=CONFIGS)
    ensure_initial_admin(config)
    if CompareOpenStackReleases(
            os_release('keystone')) >= 'liberty':
        CONFIGS.write(POLICY_JSON)
    # Ensure any existing service entries are updated in the
    # new database backend.
    update_all_identity_relation_units()
    update_all_domain_backends()


@hooks.hook('shared-db-relation-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
def db_changed():
    if 'shared-db' not in CONFIGS.complete_contexts():
        log('shared-db relation incomplete. Peer not ready?')
    else:
        CONFIGS.write(KEYSTONE_CONF)
        leader_init_db_if_ready(use_current_context=True)
        if CompareOpenStackReleases(
                os_release('keystone')) >= 'liberty':
            CONFIGS.write(POLICY_JSON)
        update_all_identity_relation_units()
        inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('shared-db-relation-departed',
            'shared-db-relation-broken')
def db_departed_or_broken():
    if is_leader():
        leader_set({'db-initialised': None})


@hooks.hook('identity-service-relation-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
def identity_changed(relation_id=None, remote_unit=None):
    notifications_checksums = {}
    notifications_endpoints = {}
    if is_leader():
        if not is_db_ready():
            log("identity-service-relation-changed hook fired before db "
                "ready - deferring until db ready", level=WARNING)
            return

        if not is_db_initialised():
            log("Database not yet initialised - deferring identity-relation "
                "updates", level=INFO)
            return

        if expect_ha() and not is_clustered():
            log("Expected to be HA but no hacluster relation yet", level=INFO)
            return

        add_service_to_keystone(relation_id, remote_unit)
        if is_service_present('neutron', 'network'):
            delete_service_entry('quantum', 'network')
        settings = relation_get(rid=relation_id, unit=remote_unit)

        # If endpoint has changed, notify to units related over the
        # identity-notifications interface. We base the decision to notify on
        # whether admin_url, public_url or internal_url have changed from
        # previous notify.
        service = settings.get('service')
        if service:
            key = '%s-endpoint-changed' % service
            notifications_endpoints[key] = endpoints_dict(settings)
            notifications_checksums[key] = endpoints_checksum(settings)
        else:
            # Some services don't set their name in the 'service' key in the
            # relation, for those their name is calculated from the prefix of
            # keys. See `assemble_endpoints()` for details.
            single = {'service', 'region', 'public_url',
                      'admin_url', 'internal_url'}
            endpoints = assemble_endpoints(settings)
            for ep in endpoints.keys():
                if single.issubset(endpoints[ep]):
                    key = '%s-endpoint-changed' % ep
                    log('endpoint: %s' % ep)
                    notifications_endpoints[key] = (
                        endpoints_dict(endpoints[ep])
                    )
                    notifications_checksums[key] = (
                        endpoints_checksum(endpoints[ep])
                    )
    else:
        log('Deferring identity_changed() to service leader.')

    if notifications_endpoints or notifications_checksums:
        send_notifications(notifications_checksums,
                           notifications_endpoints)


@hooks.hook('identity-credentials-relation-joined',
            'identity-credentials-relation-changed')
def identity_credentials_changed(relation_id=None, remote_unit=None):
    """Update the identity credentials relation on change

    Calls add_credentials_to_keystone

    :param relation_id: Relation id of the relation
    :param remote_unit: Related unit on the relation
    """
    if is_elected_leader(CLUSTER_RES):
        if expect_ha() and not is_clustered():
            log("Expected to be HA but no hacluster relation yet", level=INFO)
            return
        if not is_db_ready():
            log("identity-credentials-relation-changed hook fired before db "
                "ready - deferring until db ready", level=WARNING)
            return

        if not is_db_initialised():
            log("Database not yet initialised - deferring "
                "identity-credentials-relation updates", level=INFO)
            return

        unit_ready, _ = check_api_application_ready()
        if not unit_ready:
            log(
                ("Keystone charm unit not ready - deferring identity-relation "
                 "updates"),
                level=INFO)
            return

        # Create the tenant user
        add_credentials_to_keystone(relation_id, remote_unit)
    else:
        log('Deferring identity_credentials_changed() to service leader.')


@hooks.hook('cluster-relation-joined')
def cluster_joined(rid=None):
    settings = {}

    for addr_type in ADDRESS_TYPES:
        address = get_relation_ip(
            addr_type,
            cidr_network=config('os-{}-network'.format(addr_type)))
        if address:
            settings['{}-address'.format(addr_type)] = address

    settings['private-address'] = get_relation_ip('cluster')

    relation_set(relation_id=rid, relation_settings=settings)


@hooks.hook('cluster-relation-changed')
@restart_on_change(restart_map(), stopstart=True)
def cluster_changed():
    # NOTE(jamespage) re-echo passwords for peer storage
    echo_whitelist = ['_passwd', 'identity-service:']

    log("Peer echo whitelist: {}".format(echo_whitelist), level=DEBUG)
    peer_echo(includes=echo_whitelist, force=True)

    update_all_identity_relation_units()

    CONFIGS.write_all()
    inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('leader-elected')
@restart_on_change(restart_map(), stopstart=True)
def leader_elected():
    log('Unit has been elected leader.', level=DEBUG)
    # When the local unit has been elected the leader, update the cron jobs
    # to ensure that the cron jobs are active on this unit.
    CONFIGS.write(TOKEN_FLUSH_CRON_FILE)

    update_all_identity_relation_units()
    inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('leader-settings-changed')
@restart_on_change(restart_map(), stopstart=True)
def leader_settings_changed():

    # we always want to write the keys on leader-settings-changed regardless of
    # whether the unit is paused or not.
    if fernet_enabled():
        key_write()

    # if we are paused, delay doing any config changed hooks.
    # It is forced on the resume.
    if is_unit_paused_set():
        log("Unit is pause or upgrading. Skipping config_changed", "WARN")
        return

    # Since minions are notified of a regime change via the
    # leader-settings-changed hook, rewrite the token flush cron job to make
    # sure only the leader is running the cron job.
    CONFIGS.write(TOKEN_FLUSH_CRON_FILE)

    # Make sure we keep domain and/or project ids used in templates up to date
    if CompareOpenStackReleases(
            os_release('keystone')) >= 'liberty':
        CONFIGS.write(POLICY_JSON)

    update_all_identity_relation_units()
    inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('ha-relation-joined')
def ha_joined(relation_id=None):
    settings = generate_ha_relation_data('ks')
    relation_set(relation_id=relation_id, **settings)


@hooks.hook('ha-relation-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
def ha_changed():
    CONFIGS.write_all()

    clustered = relation_get('clustered')
    if clustered:
        log('Cluster configured, notifying other services and updating '
            'keystone endpoint configuration')
        for rid in relation_ids('certificates'):
            if related_units(rid):
                for unit in related_units(rid):
                    certs_changed(rid, unit)
        if (is_db_initialised() and is_elected_leader(CLUSTER_RES) and not
                is_unit_paused_set()):
            ensure_initial_admin(config)
            update_all_identity_relation_units()
            update_all_domain_backends()
            update_all_fid_backends()
            inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('identity-admin-relation-changed')
def admin_relation_changed(relation_id=None):
    # TODO: fixup
    if expect_ha() and not is_clustered():
        log("Expected to be HA but no hacluster relation yet", level=INFO)
        return
    relation_data = {
        'service_hostname': resolve_address(ADMIN),
        'service_port': config('service-port'),
        'service_username': config('admin-user'),
        'service_tenant_name': config('admin-role'),
        'service_region': config('region'),
        'service_protocol': 'https' if https() else 'http',
        'api_version': get_api_version(),
    }
    if relation_data['api_version'] > 2:
        relation_data['service_user_domain_name'] = ADMIN_DOMAIN
        relation_data['service_project_domain_name'] = ADMIN_DOMAIN
        relation_data['service_project_name'] = ADMIN_PROJECT
    relation_data['service_password'] = get_admin_passwd()
    relation_set(relation_id=relation_id, **relation_data)


@hooks.hook('domain-backend-relation-changed')
def domain_backend_changed(relation_id=None, unit=None):
    if get_api_version() < 3:
        log('Domain specific backend identity configuration only supported '
            'with Keystone v3 API, skipping domain creation and '
            'restart.')
        return

    domain_name = relation_get(attribute='domain-name',
                               unit=unit,
                               rid=relation_id)
    if domain_name:
        # NOTE(jamespage): Only create domain data from lead
        #                  unit when clustered and database
        #                  is configured and created.
        if is_leader() and is_db_ready() and is_db_initialised():
            create_or_show_domain(domain_name)
        # NOTE(jamespage): Deployment may have multiple domains,
        #                  with different identity backends so
        #                  ensure that a domain specific nonce
        #                  is checked for restarts of keystone
        restart_nonce = relation_get(attribute='restart-nonce',
                                     unit=unit,
                                     rid=relation_id)
        domain_nonce_key = 'domain-restart-nonce-{}'.format(domain_name)
        db = unitdata.kv()
        if restart_nonce != db.get(domain_nonce_key):
            restart_keystone()
            db.set(domain_nonce_key, restart_nonce)
            db.flush()


def configure_https():
    '''
    Enables SSL API Apache config if appropriate and kicks identity-service
    with any required api updates.
    '''
    # need to write all to ensure changes to the entire request pipeline
    # propagate (c-api, haproxy, apache)
    CONFIGS.write_all()
    # NOTE (thedac): When using snaps, nginx is installed, skip any apache2
    # config.
    if snap_install_requested():
        return
    if 'https' in CONFIGS.complete_contexts():
        cmd = ['a2ensite', 'openstack_https_frontend']
        check_call(cmd)
    else:
        cmd = ['a2dissite', 'openstack_https_frontend']
        check_call(cmd)


@hooks.hook('upgrade-charm')
@restart_on_change(restart_map(), stopstart=True)
@harden()
def upgrade_charm():
    packages_to_install = filter_installed_packages(determine_packages())
    if packages_to_install:
        log('Installing apt packages')
        status_set('maintenance', 'Installing apt packages')
        apt_install(packages_to_install)
    packages_removed = remove_old_packages()

    if run_in_apache():
        disable_unused_apache_sites()

    log('Regenerating configuration files')
    status_set('maintenance', 'Regenerating configuration files')
    CONFIGS.write_all()

    # We no longer use the admin_token and need to ensure the charm has
    # credentials.  This call is idempotent and safe to run on existing
    # deployments.
    if is_leader():
        bootstrap_keystone(configs=CONFIGS)

    # See LP bug 1519035
    leader_init_db_if_ready()

    update_nrpe_config()

    if packages_removed:
        status_set('maintenance', 'Restarting services')
        log("Package purge detected, restarting services", "INFO")
        for s in services():
            service_restart(s)
        stop_manager_instance()

    if is_elected_leader(CLUSTER_RES):
        log('Cluster leader - ensuring endpoint configuration is up to '
            'date', level=DEBUG)
        update_all_identity_relation_units()
        # also ensure that the PCI-DSS protection is in place for service
        # accounts.
        ensure_all_service_accounts_protected_for_pci_dss_options()

    # call the policy overrides handler which will install any policy overrides
    maybe_do_policyd_overrides(
        os_release('keystone'),
        'keystone',
        restart_handler=lambda: service_restart('apache2'))
    inform_peers_if_ready(check_api_unit_ready)


@hooks.hook('update-status')
@harden()
def update_status():
    log('Updating status.')


@hooks.hook('nrpe-external-master-relation-joined',
            'nrpe-external-master-relation-changed')
def update_nrpe_config():
    # python-dbus is used by check_upstart_job
    log('Updating NRPE configuration')
    status_set('maintenance', 'Updating NRPE configuration')
    apt_install('python-dbus')
    hostname = nrpe.get_nagios_hostname()
    current_unit = nrpe.get_nagios_unit_name()
    nrpe_setup = nrpe.NRPE(hostname=hostname)
    nrpe.copy_nrpe_checks()
    _services = []
    for service in services():
        if service.startswith('snap.'):
            service = service.split('.')[1]
        _services.append(service)
    nrpe.add_init_service_checks(nrpe_setup, _services, current_unit)
    nrpe.add_haproxy_checks(nrpe_setup, current_unit)
    nrpe_setup.write()


@hooks.hook('keystone-fid-service-provider-relation-joined',
            'keystone-fid-service-provider-relation-changed')
def keystone_fid_service_provider_changed():
    if get_api_version() < 3:
        log('Identity federation is only supported with keystone v3')
        return
    if CompareOpenStackReleases(os_release('keystone')) < 'ocata':
        log('Ignoring keystone-fid-service-provider relation as it is'
            ' not supported on releases older than Ocata')
        return
    # for the join case a keystone public-facing hostname and service
    # port need to be set
    update_keystone_fid_service_provider(relation_id=relation_id())

    # handle relation data updates (if any), e.g. remote_id_attribute
    # and a restart will be handled via a nonce, not restart_on_change
    CONFIGS.write(KEYSTONE_CONF)

    # The relation is container-scoped so this keystone unit's unitdata
    # will only contain a nonce of a single fid subordinate for a given
    # fid backend (relation id)
    restart_nonce = relation_get('restart-nonce')
    if restart_nonce:
        nonce = json.loads(restart_nonce)
        # multiplex by relation id for multiple federated identity
        # provider charms
        fid_nonce_key = 'fid-restart-nonce-{}'.format(relation_id())
        db = unitdata.kv()
        if restart_nonce != db.get(fid_nonce_key):
            restart_keystone()
            db.set(fid_nonce_key, nonce)
            db.flush()


@hooks.hook('keystone-fid-service-provider-relation-broken')
def keystone_fid_service_provider_broken():
    if CompareOpenStackReleases(os_release('keystone')) < 'ocata':
        log('Ignoring keystone-fid-service-provider relation as it is'
            ' not supported on releases older than Ocata')
        return

    restart_keystone()


@hooks.hook('websso-trusted-dashboard-relation-joined',
            'websso-trusted-dashboard-relation-changed',
            'websso-trusted-dashboard-relation-broken')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
def websso_trusted_dashboard_changed():
    if get_api_version() < 3:
        log('WebSSO is only supported with keystone v3')
        return
    if CompareOpenStackReleases(os_release('keystone')) < 'ocata':
        log('Ignoring WebSSO relation as it is not supported on'
            ' releases older than Ocata')
        return
    CONFIGS.write(KEYSTONE_CONF)


def update_keystone_fid_service_provider(relation_id=None):
    if relation_ids('certificates'):
        tls_enabled = True
    else:
        tls_enabled = (config('ssl_cert') is not None and
                       config('ssl_key') is not None)
    # NOTE: thedac Use resolve_address which checks host name, VIP  and
    # network bindings. Use PUBLIC for now. Possible TODO make this
    # configurable?
    hostname = resolve_address(endpoint_type=PUBLIC, override=True)
    # reactive endpoints implementation on the other side, hence
    # json-encoded values
    fid_settings = {
        'hostname': json.dumps(hostname),
        'port': json.dumps(config('service-port')),
        'tls-enabled': json.dumps(tls_enabled),
    }

    relation_set(relation_id=relation_id,
                 relation_settings=fid_settings)


@hooks.hook('certificates-relation-joined')
def certs_joined(relation_id=None):
    relation_set(
        relation_id=relation_id,
        relation_settings=get_certificate_request())


@hooks.hook('certificates-relation-changed')
@restart_on_change(restart_map(), stopstart=True)
def certs_changed(relation_id=None, unit=None):
    # update_all_identity_relation_units calls the keystone API
    # so configs need to be written and services restarted
    # before
    @restart_on_change(restart_map(), stopstart=True)
    def write_certs_and_config():
        if process_certificates('keystone', relation_id, unit):
            configure_https()
            return True
        return False
    if not write_certs_and_config():
        log('no certificates for us on the relation yet, deferring.',
            level=INFO)
        return
    # If enabling https the identity endpoints need updating.
    if (is_db_initialised() and is_elected_leader(CLUSTER_RES) and not
            is_unit_paused_set()):
        ensure_initial_admin(config)
    update_all_identity_relation_units()
    update_all_domain_backends()
    update_all_fid_backends()
    inform_peers_if_ready(check_api_unit_ready)


def notify_middleware_with_release_version():
    for rid in relation_ids('keystone-middleware'):
        relation_set(relation_id=rid, release=os_release('keystone'))


@hooks.hook('keystone-middleware-relation-joined')
def keystone_middleware_joined():
    notify_middleware_with_release_version()


@hooks.hook('keystone-middleware-relation-changed',
            'keystone-middleware-relation-broken',
            'keystone-middleware-relation-departed')
@restart_on_change(restart_map())
def keystone_middleware_changed():
    CONFIGS.write(KEYSTONE_CONF)


@hooks.hook('pre-series-upgrade')
def pre_series_upgrade():
    log("Running prepare series upgrade hook", "INFO")
    series_upgrade_prepare(
        pause_unit_helper, CONFIGS)


@hooks.hook('post-series-upgrade')
def post_series_upgrade():
    log("Running complete series upgrade hook", "INFO")
    # if we just upgraded from non systemd then ensure that the new packages of
    # keystone definitely don't run the keystone service if we are a wsgi
    # configured system.
    if run_in_apache():
        disable_unused_apache_sites()
        service_pause('keystone')
    series_upgrade_complete(
        resume_unit_helper, CONFIGS)


def main():
    try:
        hooks.execute(sys.argv)
    except UnregisteredHookError as e:
        log('Unknown hook {} - skipping.'.format(e))
    assess_status(CONFIGS)


if __name__ == '__main__':
    main()