#!/usr/bin/env python3
#
# Copyright 2016 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 os
import sys
import uuid
import shutil

from copy import deepcopy

from charmhelpers.contrib.openstack import context as os_context

from charmhelpers.contrib.openstack.utils import (
    os_restart_on_change as restart_on_change,
    series_upgrade_prepare,
    series_upgrade_complete,
    is_hook_allowed,
    CompareOpenStackReleases,
    os_release,
)

from charmhelpers.contrib.openstack.deferred_events import (
    configure_deferred_restarts,
)

from charmhelpers.core.hookenv import (
    Hooks,
    UnregisteredHookError,
    config,
    log,
    relation_set,
    relation_ids,
    charm_dir,
)

from charmhelpers.core.sysctl import create as create_sysctl

from charmhelpers.core.host import (
    is_container,
)

from charmhelpers.core.unitdata import kv
from charmhelpers.fetch import apt_install
from charmhelpers.contrib.charmsupport import nrpe

from neutron_ovs_utils import (
    DHCP_PACKAGES,
    DVR_PACKAGES,
    L3HA_PACKAGES,
    METADATA_PACKAGES,
    OVS_DEFAULT,
    USE_FQDN_KEY,
    configure_ovs,
    get_shared_secret,
    register_configs,
    restart_map,
    services,
    use_dvr,
    use_l3ha,
    enable_nova_metadata,
    enable_local_dhcp,
    install_packages,
    install_l3ha_packages,
    purge_packages,
    assess_status,
    install_tmpfilesd,
    pause_unit_helper,
    resume_unit_helper,
    determine_purge_packages,
    purge_sriov_systemd_files,
    use_fqdn_hint,
    deferrable_services,
)

hooks = Hooks()
CONFIGS = register_configs()


@hooks.hook('install.real')
def install():
    install_packages()

    # Start migration to agent registration with FQDNs for newly installed
    # units with OpenStack release Stein or newer.
    release = os_release('neutron-common')
    if CompareOpenStackReleases(release) >= 'stein':
        db = kv()
        db.set(USE_FQDN_KEY, True)
        db.flush()


# NOTE(wolsen): Do NOT add restart_on_change decorator without consideration
# for the implications of modifications to the /etc/default/openvswitch-switch.
@hooks.hook('upgrade-charm')
def upgrade_charm():
    # Tidy up any prior installation of obsolete sriov startup
    # scripts
    purge_sriov_systemd_files()

    if OVS_DEFAULT in restart_map():
        # In the 16.10 release of the charms, the code changed from managing
        # the /etc/default/openvswitch-switch file only when dpdk was enabled
        # to always managing this file. Thus, an upgrade of the charm from a
        # release prior to 16.10 or higher will always cause the contents of
        # the file to change and will trigger a restart of the
        # openvswitch-switch service, which in turn causes a temporary
        # network outage. To prevent this outage, determine if the
        # /etc/default/openvswitch-switch file needs to be migrated and if
        # so, migrate the file but do NOT restart the openvswitch-switch
        # service.
        # See bug LP #1712444
        with open(OVS_DEFAULT, 'r') as f:
            # The 'Service restart triggered ...' line was added to the
            # OVS_DEFAULT template in the 16.10 version of the charm to allow
            # restarts so we use this as the key to see if the file needs
            # migrating.
            if 'Service restart triggered' not in f.read():
                CONFIGS.write(OVS_DEFAULT)


@hooks.hook('neutron-plugin-relation-changed')
@hooks.hook('config-changed')
# NOTE(fnordahl): we need to act immediately to changes to OVS_DEFAULT in-line
# so ignore it here to avoid restarting the services twice. LP: #1906280
@restart_on_change({cfg: services
                    for cfg, services in restart_map().items()
                    if cfg != OVS_DEFAULT})
def config_changed(check_deferred_restarts=True):
    configure_deferred_restarts(deferrable_services())
    # policy_rcd.remove_policy_file()
    # if we are paused, delay doing any config changed hooks.
    # It is forced on the resume.
    allowed, reason = is_hook_allowed(
        'config-changed',
        check_deferred_restarts=check_deferred_restarts)
    if not allowed:
        log(reason, "WARN")
        return

    install_packages()
    install_tmpfilesd()

    # NOTE(jamespage): purge any packages as a result of py3 switch
    #                  at rocky.
    packages_to_purge = determine_purge_packages()
    request_nova_compute_restart = False
    if packages_to_purge:
        purge_packages(packages_to_purge)
        request_nova_compute_restart = True

    sysctl_settings = config('sysctl')
    if not is_container() and sysctl_settings:
        create_sysctl(sysctl_settings,
                      '/etc/sysctl.d/50-openvswitch.conf')

    # NOTE(fnordahl): It is important to write config to disk and perhaps
    # restart the openvswitch-swith service prior to attempting to do run-time
    # configuration of OVS as we may have to pass options to `ovs-ctl` for
    # `ovs-vswitchd` to run at all. LP: #1906280
    # TODO: make restart_on_change use contextlib.contextmanager
    @restart_on_change({cfg: services
                        for cfg, services in restart_map().items()
                        if cfg == OVS_DEFAULT})
    def _restart_before_runtime_config_when_required():
        CONFIGS.write_all()
    _restart_before_runtime_config_when_required()
    configure_ovs()

    for rid in relation_ids('neutron-plugin'):
        neutron_plugin_joined(
            relation_id=rid,
            request_restart=request_nova_compute_restart)

    update_nrpe_config()


@hooks.hook('neutron-plugin-api-relation-changed')
# NOTE(fnordahl): we need to act immediately to changes to OVS_DEFAULT in-line
# so ignore it here to avoid restarting the services twice. LP: #1906280
@restart_on_change({cfg: services
                    for cfg, services in restart_map().items()
                    if cfg != OVS_DEFAULT})
def neutron_plugin_api_changed():
    packages_to_purge = []
    if use_dvr():
        install_packages()
        # per 17.08 release notes L3HA + DVR is a Newton+ feature
        _os_release = os_release('neutron-common', base='icehouse')
        if (use_l3ha() and
                CompareOpenStackReleases(_os_release) >= 'newton'):
            install_l3ha_packages()

        # NOTE(hopem): don't uninstall keepalived if not using l3ha since that
        # results in neutron-l3-agent also being uninstalled (see LP 1819499).
    else:
        packages_to_purge = deepcopy(DVR_PACKAGES)
        packages_to_purge.extend(L3HA_PACKAGES)

    if packages_to_purge:
        purge_packages(packages_to_purge)

    # NOTE(fnordahl): It is important to write config to disk and perhaps
    # restart the openvswitch-swith service prior to attempting to do run-time
    # configuration of OVS as we may have to pass options to `ovs-ctl` for
    # `ovs-vswitchd` to run at all. LP: #1906280
    # TODO: make restart_on_change use contextlib.contextmanager
    @restart_on_change({cfg: service
                        for cfg, service in restart_map().items()
                        if cfg == OVS_DEFAULT})
    def _restart_before_runtime_config_when_required():
        CONFIGS.write_all()
    _restart_before_runtime_config_when_required()
    configure_ovs()
    # If dvr setting has changed, need to pass that on
    for rid in relation_ids('neutron-plugin'):
        neutron_plugin_joined(relation_id=rid)


@hooks.hook('neutron-plugin-relation-joined')
def neutron_plugin_joined(relation_id=None, request_restart=False):
    secret = None
    if not is_container():
        if enable_local_dhcp():
            install_packages()
        else:
            pkgs = deepcopy(DHCP_PACKAGES)
            # NOTE: only purge metadata packages if dvr is not
            #       in use as this will remove the l3 agent
            #       see https://pad.lv/1515008
            if not use_dvr():
                # NOTE(fnordahl) do not remove ``haproxy``, the principal
                # charm may have use for it. LP: #1832739
                pkgs.extend(set(METADATA_PACKAGES)-set(['haproxy']))
            purge_packages(pkgs)
        secret = get_shared_secret() if enable_nova_metadata() else None
    rel_data = {
        'metadata-shared-secret': secret,
    }
    host_info = os_context.HostInfoContext()()
    if use_fqdn_hint() and host_info.get('host_fqdn'):
        rel_data.update({'host': host_info['host_fqdn']})
    if request_restart:
        rel_data['restart-nonce'] = str(uuid.uuid4())
    relation_set(relation_id=relation_id, **rel_data)


@hooks.hook('amqp-relation-joined')
def amqp_joined(relation_id=None):
    relation_set(relation_id=relation_id,
                 username=config('rabbit-user'),
                 vhost=config('rabbit-vhost'))


@hooks.hook('amqp-relation-changed')
@hooks.hook('amqp-relation-departed')
@restart_on_change(restart_map())
def amqp_changed():
    if 'amqp' not in CONFIGS.complete_contexts():
        log('amqp relation incomplete. Peer not ready?')
        return
    CONFIGS.write_all()


@hooks.hook('neutron-control-relation-changed')
@restart_on_change(restart_map(),
                   stopstart=True)
def restart_check():
    CONFIGS.write_all()


@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")
    series_upgrade_complete(
        resume_unit_helper, CONFIGS)


def install_nrpe_cron():
    src = os.path.join(charm_dir(), "files", "ovs_vsctl", "cron_ovs_vsctl.sh")
    dst = shutil.copy(src, "/usr/local/lib/nagios/plugins/")
    os.chmod(dst, 0o100755)
    os.chown(dst, uid=0, gid=0)

    cronjob_line = "3 * * * * root {cmd}\n".format(cmd=dst)
    crond_file = "/etc/cron.d/neutron_openvswitch_ovs_vsctl"
    with open(crond_file, "w") as crond_fd:
        crond_fd.write(cronjob_line)
    return dst


def install_nrpe_plugin():
    src = os.path.join(charm_dir(), "files", "ovs_vsctl", "check_ovs_vsctl.py")
    dst = shutil.copy(src, "/usr/local/lib/nagios/plugins")
    os.chmod(dst, 0o100755)
    os.chown(dst, uid=0, gid=0)
    return dst


@hooks.hook('nrpe-external-master-relation-joined',
            'nrpe-external-master-relation-changed')
def update_nrpe_config():
    # python-dbus is used by check_upstart_job
    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()
    nrpe.add_init_service_checks(nrpe_setup, services(), current_unit)
    install_nrpe_cron()
    cmd = install_nrpe_plugin()
    nrpe_setup.add_check(
        shortname="ovs_vsctl",
        description="Check ovs-vsctl list-br for predictable operation.",
        check_cmd=cmd
    )
    nrpe_setup.write()


@hooks.hook('update-status')
def dummy_update_status():
    """Dummy function to silence missing hook log entry"""
    pass


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


if __name__ == '__main__':
    main()