"""
Task for dnsmasq configuration
"""
import contextlib
import logging

from teuthology import misc
from teuthology.exceptions import ConfigError
from teuthology import contextutil
from teuthology import packaging
from tasks.util import get_remote_for_role

log = logging.getLogger(__name__)

@contextlib.contextmanager
def install_dnsmasq(remote):
    """
    If dnsmasq is not installed, install it for the duration of the task.
    """
    try:
        existing = packaging.get_package_version(remote, 'dnsmasq')
    except:
        existing = None

    if existing is None:
        packaging.install_package('dnsmasq', remote)
    try:
        yield
    finally:
        if existing is None:
            packaging.remove_package('dnsmasq', remote)

@contextlib.contextmanager
def backup_resolv(remote, path):
    """
    Store a backup of resolv.conf in the testdir and restore it after the task.
    """
    remote.run(args=['cp', '/etc/resolv.conf', path])
    try:
        yield
    finally:
        # restore with 'cp' to avoid overwriting its security context
        remote.run(args=['sudo', 'cp', path, '/etc/resolv.conf'])
        remote.run(args=['rm', path])

@contextlib.contextmanager
def replace_resolv(remote, path):
    """
    Update resolv.conf to point the nameserver at localhost.
    """
    remote.write_file(path, "nameserver 127.0.0.1\n")
    try:
        # install it
        if remote.os.package_type == "rpm":
            # for centos ovh resolv.conf has immutable attribute set
            remote.run(args=['sudo', 'chattr', '-i', '/etc/resolv.conf'], check_status=False)
        remote.run(args=['sudo', 'cp', path, '/etc/resolv.conf'])
        yield
    finally:
        remote.run(args=['rm', path])

@contextlib.contextmanager
def setup_dnsmasq(remote, testdir, cnames):
    """ configure dnsmasq on the given remote, adding each cname given """
    log.info('Configuring dnsmasq on remote %s..', remote.name)

    # add address entries for each cname
    dnsmasq = "server=8.8.8.8\nserver=8.8.4.4\n"
    address_template = "address=/{cname}/{ip_address}\n"
    for cname, ip_address in cnames.items():
        dnsmasq += address_template.format(cname=cname, ip_address=ip_address)

    # write to temporary dnsmasq file
    dnsmasq_tmp = '/'.join((testdir, 'ceph.tmp'))
    remote.write_file(dnsmasq_tmp, dnsmasq)

    # move into /etc/dnsmasq.d/
    dnsmasq_path = '/etc/dnsmasq.d/ceph'
    remote.run(args=['sudo', 'mv', dnsmasq_tmp, dnsmasq_path])
    # restore selinux context if necessary
    remote.run(args=['sudo', 'restorecon', dnsmasq_path], check_status=False)

    # restart dnsmasq
    remote.run(args=['sudo', 'systemctl', 'restart', 'dnsmasq'])
    # verify dns name is set
    remote.run(args=['ping', '-c', '4', next(iter(cnames.keys()))])

    try:
        yield
    finally:
        log.info('Removing dnsmasq configuration from remote %s..', remote.name)
        # remove /etc/dnsmasq.d/ceph
        remote.run(args=['sudo', 'rm', dnsmasq_path])
        # restart dnsmasq
        remote.run(args=['sudo', 'systemctl', 'restart', 'dnsmasq'])

@contextlib.contextmanager
def task(ctx, config):
    """
    Configures dnsmasq to add cnames for teuthology remotes. The task expects a
    dictionary, where each key is a role. If all cnames for that role use the
    same address as that role, the cnames can be given as a list. For example,
    this entry configures dnsmasq on the remote associated with client.0, adding
    two cnames for the ip address associated with client.0:

        - dnsmasq:
            client.0:
            - client0.example.com
            - c0.example.com

    If the addresses do not all match the given role, a dictionary can be given
    to specify the ip address by its target role. For example:

        - dnsmasq:
            client.0:
              client.0.example.com: client.0
              client.1.example.com: client.1

    Cnames that end with a . are treated as prefix for the existing hostname.
    For example, if the remote for client.0 has a hostname of 'example.com',
    this task will add cnames for dev.example.com and test.example.com:

        - dnsmasq:
            client.0: [dev., test.]
    """
    # apply overrides
    overrides = config.get('overrides', {})
    misc.deep_merge(config, overrides.get('dnsmasq', {}))

    # multiple roles may map to the same remote, so collect names by remote
    remote_names = {}
    for role, cnames in config.items():
        remote = get_remote_for_role(ctx, role)
        if remote is None:
            raise ConfigError('no remote for role %s' % role)

        names = remote_names.get(remote, {})

        if isinstance(cnames, list):
            # when given a list of cnames, point to local ip
            for cname in cnames:
                if cname.endswith('.'):
                    cname += remote.hostname
                names[cname] = remote.ip_address
        elif isinstance(cnames, dict):
            # when given a dict, look up the remote ip for each
            for cname, client in cnames.items():
                r = get_remote_for_role(ctx, client)
                if r is None:
                    raise ConfigError('no remote for role %s' % client)
                if cname.endswith('.'):
                    cname += r.hostname
                names[cname] = r.ip_address

        remote_names[remote] = names

    testdir = misc.get_testdir(ctx)
    resolv_bak = '/'.join((testdir, 'resolv.bak'))
    resolv_tmp = '/'.join((testdir, 'resolv.tmp'))

    # run subtasks for each unique remote
    subtasks = []
    for remote, cnames in remote_names.items():
        subtasks.extend([ lambda r=remote: install_dnsmasq(r) ])
        subtasks.extend([ lambda r=remote: backup_resolv(r, resolv_bak) ])
        subtasks.extend([ lambda r=remote: replace_resolv(r, resolv_tmp) ])
        subtasks.extend([ lambda r=remote, cn=cnames: setup_dnsmasq(r, testdir, cn) ])

    with contextutil.nested(*subtasks):
        yield
