#!/usr/bin/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 base64
import copy
import json
import os
import shutil
import sys
import socket
import subprocess
import tempfile
import yaml

from subprocess import CalledProcessError

_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 lib.swift_storage_utils import (
    restart_map,
    SWIFT_SVCS,
    determine_packages,
    do_openstack_upgrade,
    ensure_swift_directories,
    fetch_swift_rings,
    register_configs,
    save_script_rc,
    setup_storage,
    assert_charm_supports_ipv6,
    setup_rsync,
    remember_devices,
    REQUIRED_INTERFACES,
    assess_status,
    ensure_devs_tracked,
    VERSION_PACKAGE,
    setup_ufw,
    revoke_access,
    enable_replication,
)

from lib.misc_utils import pause_aware_restart_on_change

from charmhelpers.core.hookenv import (
    Hooks, UnregisteredHookError,
    config,
    log,
    network_get_primary_address,
    relation_get,
    relation_ids,
    relation_set,
    relations_of_type,
    status_set,
    ingress_address,
    DEBUG,
    WARNING,
    ERROR,
)

from charmhelpers.fetch import (
    apt_install,
    apt_update,
    filter_installed_packages
)
from charmhelpers.core.host import (
    add_to_updatedb_prunepath,
    rsync,
    write_file,
    umount,
    service_start,
    service_stop,
)

from charmhelpers.core.sysctl import create as create_sysctl
from charmhelpers.contrib.sysctl.watermark_scale_factor import (
    calculate_watermark_scale_factor,
)

from charmhelpers.payload.execd import execd_preinstall

from charmhelpers.contrib.openstack.utils import (
    configure_installation_source,
    openstack_upgrade_available,
    set_os_workload_status,
    os_application_version_set,
    clear_unit_paused,
    clear_unit_upgrading,
    is_unit_paused_set,
    set_unit_paused,
    set_unit_upgrading,
    get_os_codename_install_source,
)
from charmhelpers.contrib.network.ip import (
    get_relation_ip,
)
from charmhelpers.contrib.network import ufw
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.contrib.hardening.harden import harden
from charmhelpers.core.unitdata import kv

from distutils.dir_util import mkpath

import charmhelpers.contrib.openstack.vaultlocker as vaultlocker

hooks = Hooks()
CONFIGS = register_configs()
NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
SUDOERS_D = '/etc/sudoers.d'
STORAGE_MOUNT_PATH = '/srv/node'
UFW_DIR = '/etc/ufw'


def add_ufw_gre_rule(ufw_rules_path):
    """Add allow gre rule to UFW

    Make a copy of existing UFW before rules, insert our new rule and replace
    existing rules with updated version.
    """
    rule = '-A ufw-before-input -p 47 -j ACCEPT'
    rule_exists = False
    with tempfile.NamedTemporaryFile() as tmpfile:
        # Close pre-opened file so that we can replace it with a copy of our
        # config file.
        tmpfile.file.close()
        dst = tmpfile.name
        # Copy over ufw rules file
        shutil.copyfile(ufw_rules_path, dst)
        with open(dst, 'r') as fd:
            lines = fd.readlines()

        # Check whether the line we are adding already exists
        for line in lines:
            if rule in line:
                rule_exists = True
                break

        added = False
        if not rule_exists:
            # Insert our rule as the first rule.
            with open(dst, 'w') as fd:
                for line in lines:
                    if not added and line.startswith('-A'):
                        fd.write('# Allow GRE Traffic (added by swift-storage '
                                 'charm)\n')
                        fd.write('{}\n'.format(rule))
                        fd.write(line)
                        added = True
                    else:
                        fd.write(line)

            # Replace existing config with updated one.
            shutil.copyfile(dst, ufw_rules_path)


def initialize_ufw():
    """Initialize the UFW firewall

    Ensure critical ports have explicit allows

    :return: None
    """

    if not config('enable-firewall'):
        log("Firewall has been administratively disabled", "DEBUG")
        return

    # this charm will monitor exclusively the ports used, using 'allow' as
    # default policy enables sharing the machine with other services
    ufw.default_policy('allow', 'incoming')
    ufw.default_policy('allow', 'outgoing')
    ufw.default_policy('allow', 'routed')
    # Rsync manages its own ACLs
    ufw.service('rsync', 'open')
    # Guarantee SSH access
    ufw.service('ssh', 'open')
    # Enable
    ufw.enable(soft_fail=config('allow-ufw-ip6-softfail'))

    # Allow GRE traffic
    add_ufw_gre_rule(os.path.join(UFW_DIR, 'before.rules'))
    ufw.reload()


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


@hooks.hook('config-changed')
@pause_aware_restart_on_change(restart_map())
@harden()
def config_changed():
    if config('enable-firewall'):
        initialize_ufw()
    else:
        ufw.disable()

    if config('ephemeral-unmount'):
        umount(config('ephemeral-unmount'), persist=True)

    if config('prefer-ipv6'):
        status_set('maintenance', 'Configuring ipv6')
        assert_charm_supports_ipv6()

    ensure_swift_directories()
    setup_rsync()

    if not config('action-managed-upgrade') and \
            openstack_upgrade_available('swift'):
        status_set('maintenance', 'Running openstack upgrade')
        do_openstack_upgrade(configs=CONFIGS)

    install_vaultlocker()

    configure_storage()

    CONFIGS.write_all()

    save_script_rc()
    if relations_of_type('nrpe-external-master'):
        update_nrpe_config()

    if config('sysctl'):
        sysctl_dict_parsed = {}
        sysctl_settings = config('sysctl')
        try:
            sysctl_dict_parsed = yaml.safe_load(sysctl_settings)
        except yaml.YAMLError:
            log("Error parsing YAML sysctl_dict: {}".format(sysctl_settings),
                level=ERROR)
        if (config('tune-watermark-scale-factor') is True and
                "vm.watermark_scale_factor" not in sysctl_dict_parsed):
            try:
                wmark = calculate_watermark_scale_factor()
                sysctl_dict_parsed["vm.watermark_scale_factor"] = wmark
            except Exception:
                pass

        if sysctl_dict_parsed:
            create_sysctl(sysctl_dict_parsed,
                          '/etc/sysctl.d/50-swift-storage-charm.conf')

    add_to_updatedb_prunepath(STORAGE_MOUNT_PATH)


def install_vaultlocker():
    """Determine whether vaultlocker is required and install"""
    if config('encrypt'):
        pkgs = ['vaultlocker']
        installed = len(filter_installed_packages(pkgs)) == 0
        if not installed:
            apt_install(pkgs, fatal=True)


@hooks.hook('upgrade-charm.real')
@harden()
def upgrade_charm():
    initialize_ufw()
    rel = get_os_codename_install_source(config('openstack-origin'))
    pkgs = determine_packages(rel)
    apt_install(pkgs, fatal=True)
    update_nrpe_config()
    ensure_devs_tracked()


@hooks.hook()
def swift_storage_relation_joined(rid=None):
    if config('encrypt') and not vaultlocker.vault_relation_complete():
        log('Encryption configured and vault not ready, deferring',
            level=DEBUG)
        return
    rel_settings = {
        'zone': config('zone'),
        'object_port': config('object-server-port'),
        'container_port': config('container-server-port'),
        'account_port': config('account-server-port'),
    }
    if enable_replication():
        replication_ip = network_get_primary_address('replication')
        cluster_ip = network_get_primary_address('cluster')
        rel_settings.update({
            'ip_rep': replication_ip,
            'ip_cls': cluster_ip,
            'region': config('storage-region'),
            'object_port_rep': config('object-server-port-rep'),
            'container_port_rep': config('container-server-port-rep'),
            'account_port_rep': config('account-server-port-rep')})
    db = kv()
    devs = db.get('prepared-devices', [])
    devs = [os.path.basename(d) for d in devs]
    rel_settings['device'] = ':'.join(devs)
    # Keep a reference of devices we are adding to the ring
    remember_devices(devs)

    rel_settings['private-address'] = get_relation_ip('swift-storage')

    relation_set(relation_id=rid, relation_settings=rel_settings)


@hooks.hook('swift-storage-relation-changed')
@pause_aware_restart_on_change(restart_map())
def swift_storage_relation_changed():
    setup_ufw()
    rings_url = relation_get('rings_url')
    swift_hash = relation_get('swift_hash')
    if '' in [rings_url, swift_hash] or None in [rings_url, swift_hash]:
        log('swift_storage_relation_changed: Peer not ready?')
        sys.exit(0)

    CONFIGS.write('/etc/rsync-juju.d/050-swift-storage.conf')
    CONFIGS.write('/etc/swift/swift.conf')

    # NOTE(hopem): retries are handled in the function but it is possible that
    #              we are attempting to get rings from a proxy that is no
    #              longer publiscising them so lets catch the error, log a
    #              message and hope that the good rings_url us waiting to be
    #              consumed.
    try:
        fetch_swift_rings(rings_url)
    except CalledProcessError:
        log("Failed to sync rings from {} - no longer available from that "
            "unit?".format(rings_url), level=WARNING)


@hooks.hook('swift-storage-relation-departed')
def swift_storage_relation_departed():
    ports = [config('object-server-port'),
             config('container-server-port'),
             config('account-server-port')]
    removed_client = ingress_address()
    if removed_client:
        for port in ports:
            revoke_access(removed_client, port)


@hooks.hook('secrets-storage-relation-joined')
def secrets_storage_joined(relation_id=None):
    relation_set(relation_id=relation_id,
                 secret_backend='charm-vaultlocker',
                 isolated=True,
                 access_address=get_relation_ip('secrets-storage'),
                 hostname=socket.gethostname())


@hooks.hook('secrets-storage-relation-changed')
def secrets_storage_changed():
    vault_ca = relation_get('vault_ca')
    if vault_ca:
        vault_ca = base64.decodebytes(json.loads(vault_ca).encode())
        write_file('/usr/local/share/ca-certificates/vault-ca.crt',
                   vault_ca, perms=0o644)
        subprocess.check_call(['update-ca-certificates', '--fresh'])
    configure_storage()


@hooks.hook('storage.real')
def configure_storage():
    setup_storage(config('encrypt'))

    for rid in relation_ids('swift-storage'):
        swift_storage_relation_joined(rid=rid)


@hooks.hook('nrpe-external-master-relation-joined')
@hooks.hook('nrpe-external-master-relation-changed')
def update_nrpe_config():
    # python-dbus is used by check_upstart_job
    apt_install('python-dbus')
    log('Refreshing nrpe checks')
    if not os.path.exists(NAGIOS_PLUGINS):
        mkpath(NAGIOS_PLUGINS)
    rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nrpe-external-master',
                       'check_swift_storage.py'),
          os.path.join(NAGIOS_PLUGINS, 'check_swift_storage.py'))
    rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nrpe-external-master',
                       'check_timed_logs.pl'),
          os.path.join(NAGIOS_PLUGINS, 'check_timed_logs.pl'))
    rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nrpe-external-master',
                       'check_swift_replicator_logs.sh'),
          os.path.join(NAGIOS_PLUGINS, 'check_swift_replicator_logs.sh'))
    rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nrpe-external-master',
                       'check_swift_service'),
          os.path.join(NAGIOS_PLUGINS, 'check_swift_service'))
    rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'sudo',
                       'swift-storage'),
          os.path.join(SUDOERS_D, 'swift-storage'))

    # Find out if nrpe set nagios_hostname
    hostname = nrpe.get_nagios_hostname()
    current_unit = nrpe.get_nagios_unit_name()
    nrpe_setup = nrpe.NRPE(hostname=hostname)

    # check the rings and replication
    nrpe_setup.add_check(
        shortname='swift_storage',
        description='Check swift storage ring hashes and replication'
                    ' {%s}' % current_unit,
        check_cmd='check_swift_storage.py {}'.format(
            config('nagios-check-params'))
    )

    object_port = config('object-server-port')
    container_port = config('container-server-port')
    account_port = config('account-server-port')

    nrpe_setup.add_check(
        shortname="swift-object-server-api",
        description="Check Swift Object Server API availability",
        check_cmd="/usr/lib/nagios/plugins/check_http \
                  -I localhost -u /recon/version -p {} \
                  -e \"OK\"".format(object_port))

    nrpe_setup.add_check(
        shortname="swift-container-server-api",
        description="Check Swift Container Server API availability",
        check_cmd="/usr/lib/nagios/plugins/check_http \
                  -I localhost -u /recon/version -p {} \
                  -e \"OK\"".format(container_port))

    nrpe_setup.add_check(
        shortname="swift-account-server-api",
        description="Check Swift Account Server API availability",
        check_cmd="/usr/lib/nagios/plugins/check_http \
                  -I localhost -u /recon/version -p {} \
                  -e \"OK\"".format(account_port))

    if config('nagios-replication-check-params'):
        nrpe_setup.add_check(
            shortname='swift_replicator_health',
            description='Check swift object replicator log reporting',
            check_cmd='check_swift_replicator_logs.sh {}'.format(
                config('nagios-replication-check-params'))
        )
    else:
        nrpe_setup.remove_check(shortname='swift_replicator_health')

    nrpe.add_init_service_checks(nrpe_setup, SWIFT_SVCS, current_unit)
    nrpe_setup.write()


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


@hooks.hook('pre-series-upgrade')
def pre_series_upgrade():
    log("Running prepare series upgrade hook", "INFO")
    if not is_unit_paused_set():
        for service in SWIFT_SVCS:
            stopped = service_stop(service)
            if not stopped:
                raise Exception("{} didn't stop cleanly.".format(service))
        set_unit_paused()
    set_unit_upgrading()


@hooks.hook('post-series-upgrade')
def post_series_upgrade():
    log("Running complete series upgrade hook", "INFO")
    clear_unit_paused()
    clear_unit_upgrading()
    if not is_unit_paused_set():
        for service in SWIFT_SVCS:
            started = service_start(service)
            if not started:
                raise Exception("{} didn't start cleanly.".format(service))


def main():
    try:
        hooks.execute(sys.argv)
    except UnregisteredHookError as e:
        log('Unknown hook {} - skipping.'.format(e))
    required_interfaces = copy.deepcopy(REQUIRED_INTERFACES)

    if config('encrypt') and \
            len(filter_installed_packages('vaultlocker')) == 0:
        required_interfaces['vault'] = ['secrets-storage']

    set_os_workload_status(CONFIGS, required_interfaces,
                           charm_func=assess_status)
    os_application_version_set(VERSION_PACKAGE)


if __name__ == '__main__':
    main()