# Copyright 2014-2016 Canonical Limited.
#
# 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.

#
# Copyright 2016 Canonical Ltd.
#
# Authors:
#  Openstack Charmers <
#

"""
Helpers for high availability.
"""

import json

import re

from charmhelpers.core.hookenv import (
    log,
    relation_set,
    charm_name,
    config,
    status_set,
    DEBUG,
    WARNING,
)

from charmhelpers.core.host import (
    lsb_release
)

from charmhelpers.contrib.openstack.ip import (
    resolve_address,
    is_ipv6,
)

from charmhelpers.contrib.network.ip import (
    get_iface_for_address,
    get_netmask_for_address,
)

from charmhelpers.contrib.hahelpers.cluster import (
    get_hacluster_config
)

JSON_ENCODE_OPTIONS = dict(
    sort_keys=True,
    allow_nan=False,
    indent=None,
    separators=(',', ':'),
)


class DNSHAException(Exception):
    """Raised when an error occurs setting up DNS HA
    """

    pass


def update_dns_ha_resource_params(resources, resource_params,
                                  relation_id=None,
                                  crm_ocf='ocf:maas:dns'):
    """ Configure DNS-HA resources based on provided configuration and
    update resource dictionaries for the HA relation.

    @param resources: Pointer to dictionary of resources.
                      Usually instantiated in ha_joined().
    @param resource_params: Pointer to dictionary of resource parameters.
                            Usually instantiated in ha_joined()
    @param relation_id: Relation ID of the ha relation
    @param crm_ocf: Corosync Open Cluster Framework resource agent to use for
                    DNS HA
    """
    _relation_data = {'resources': {}, 'resource_params': {}}
    update_hacluster_dns_ha(charm_name(),
                            _relation_data,
                            crm_ocf)
    resources.update(_relation_data['resources'])
    resource_params.update(_relation_data['resource_params'])
    relation_set(relation_id=relation_id, groups=_relation_data['groups'])


def assert_charm_supports_dns_ha():
    """Validate prerequisites for DNS HA
    The MAAS client is only available on Xenial or greater

    :raises DNSHAException: if release is < 16.04
    """
    if lsb_release().get('DISTRIB_RELEASE') < '16.04':
        msg = ('DNS HA is only supported on 16.04 and greater '
               'versions of Ubuntu.')
        status_set('blocked', msg)
        raise DNSHAException(msg)
    return True


def expect_ha():
    """ Determine if the unit expects to be in HA

    Check for VIP or dns-ha settings which indicate the unit should expect to
    be related to hacluster.

    @returns boolean
    """
    return config('vip') or config('dns-ha')


def generate_ha_relation_data(service):
    """ Generate relation data for ha relation

    Based on configuration options and unit interfaces, generate a json
    encoded dict of relation data items for the hacluster relation,
    providing configuration for DNS HA or VIP's + haproxy clone sets.

    @returns dict: json encoded data for use with relation_set
    """
    _haproxy_res = 'res_{}_haproxy'.format(service)
    _relation_data = {
        'resources': {
            _haproxy_res: 'lsb:haproxy',
        },
        'resource_params': {
            _haproxy_res: 'op monitor interval="5s"'
        },
        'init_services': {
            _haproxy_res: 'haproxy'
        },
        'clones': {
            'cl_{}_haproxy'.format(service): _haproxy_res
        },
    }

    if config('dns-ha'):
        update_hacluster_dns_ha(service, _relation_data)
    else:
        update_hacluster_vip(service, _relation_data)

    return {
        'json_{}'.format(k): json.dumps(v, **JSON_ENCODE_OPTIONS)
        for k, v in _relation_data.items() if v
    }


def update_hacluster_dns_ha(service, relation_data,
                            crm_ocf='ocf:maas:dns'):
    """ Configure DNS-HA resources based on provided configuration

    @param service: Name of the service being configured
    @param relation_data: Pointer to dictionary of relation data.
    @param crm_ocf: Corosync Open Cluster Framework resource agent to use for
                    DNS HA
    """
    # Validate the charm environment for DNS HA
    assert_charm_supports_dns_ha()

    settings = ['os-admin-hostname', 'os-internal-hostname',
                'os-public-hostname', 'os-access-hostname']

    # Check which DNS settings are set and update dictionaries
    hostname_group = []
    for setting in settings:
        hostname = config(setting)
        if hostname is None:
            log('DNS HA: Hostname setting {} is None. Ignoring.'
                ''.format(setting),
                DEBUG)
            continue
        m = re.search('os-(.+?)-hostname', setting)
        if m:
            endpoint_type = m.group(1)
            # resolve_address's ADDRESS_MAP uses 'int' not 'internal'
            if endpoint_type == 'internal':
                endpoint_type = 'int'
        else:
            msg = ('Unexpected DNS hostname setting: {}. '
                   'Cannot determine endpoint_type name'
                   ''.format(setting))
            status_set('blocked', msg)
            raise DNSHAException(msg)

        hostname_key = 'res_{}_{}_hostname'.format(service, endpoint_type)
        if hostname_key in hostname_group:
            log('DNS HA: Resource {}: {} already exists in '
                'hostname group - skipping'.format(hostname_key, hostname),
                DEBUG)
            continue

        hostname_group.append(hostname_key)
        relation_data['resources'][hostname_key] = crm_ocf
        relation_data['resource_params'][hostname_key] = (
            'params fqdn="{}" ip_address="{}"'
            .format(hostname, resolve_address(endpoint_type=endpoint_type,
                                              override=False)))

    if len(hostname_group) >= 1:
        log('DNS HA: Hostname group is set with {} as members. '
            'Informing the ha relation'.format(' '.join(hostname_group)),
            DEBUG)
        relation_data['groups'] = {
            'grp_{}_hostnames'.format(service): ' '.join(hostname_group)
        }
    else:
        msg = 'DNS HA: Hostname group has no members.'
        status_set('blocked', msg)
        raise DNSHAException(msg)


def update_hacluster_vip(service, relation_data):
    """ Configure VIP resources based on provided configuration

    @param service: Name of the service being configured
    @param relation_data: Pointer to dictionary of relation data.
    """
    cluster_config = get_hacluster_config()
    vip_group = []
    for vip in cluster_config['vip'].split():
        if is_ipv6(vip):
            res_neutron_vip = 'ocf:heartbeat:IPv6addr'
            vip_params = 'ipv6addr'
        else:
            res_neutron_vip = 'ocf:heartbeat:IPaddr2'
            vip_params = 'ip'

        iface = (get_iface_for_address(vip) or
                 config('vip_iface'))
        netmask = (get_netmask_for_address(vip) or
                   config('vip_cidr'))

        if iface is not None:
            vip_key = 'res_{}_{}_vip'.format(service, iface)
            if vip_key in vip_group:
                if vip not in relation_data['resource_params'][vip_key]:
                    vip_key = '{}_{}'.format(vip_key, vip_params)
                else:
                    log("Resource '%s' (vip='%s') already exists in "
                        "vip group - skipping" % (vip_key, vip), WARNING)
                    continue

            relation_data['resources'][vip_key] = res_neutron_vip
            relation_data['resource_params'][vip_key] = (
                'params {ip}="{vip}" cidr_netmask="{netmask}" '
                'nic="{iface}"'.format(ip=vip_params,
                                       vip=vip,
                                       iface=iface,
                                       netmask=netmask)
            )
            vip_group.append(vip_key)

    if len(vip_group) >= 1:
        relation_data['groups'] = {
            'grp_{}_vips'.format(service): ' '.join(vip_group)
        }
