# -*- coding: utf-8 -*-
'''
Compendium of generic DNS utilities
# Examples:
dns.lookup(name, rdtype, ...)
dns.query(name, rdtype, ...)

dns.srv_rec(data)
dns.srv_data('my1.example.com', 389, prio=10, weight=100)
dns.srv_name('ldap/tcp', 'example.com')

'''
from __future__ import absolute_import, print_function, unicode_literals

# Import Python libs
import base64
import binascii
import hashlib
import itertools
import logging
import random
import re
import shlex
import socket
import ssl
import string
import functools

# Import Salt libs
import salt.utils.files
import salt.utils.network
import salt.utils.path
import salt.utils.stringutils
import salt.modules.cmdmod
from salt._compat import ipaddress
from salt.utils.odict import OrderedDict

# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves import zip  # pylint: disable=redefined-builtin

# Integrations
try:
    import dns.resolver
    HAS_DNSPYTHON = True
except ImportError:
    HAS_DNSPYTHON = False
try:
    import tldextract
    HAS_TLDEXTRACT = True
except ImportError:
    HAS_TLDEXTRACT = False
HAS_DIG = salt.utils.path.which('dig') is not None
DIG_OPTIONS = '+search +fail +noall +answer +nocl +nottl'
HAS_DRILL = salt.utils.path.which('drill') is not None
HAS_HOST = salt.utils.path.which('host') is not None
HAS_NSLOOKUP = salt.utils.path.which('nslookup') is not None

__salt__ = {
    'cmd.run_all': salt.modules.cmdmod.run_all
}
log = logging.getLogger(__name__)


class RFC(object):
    '''
    Simple holding class for all RFC/IANA registered lists & standards
    '''
    # https://tools.ietf.org/html/rfc6844#section-3
    CAA_TAGS = (
        'issue',
        'issuewild',
        'iodef'
    )

    # http://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
    SSHFP_ALGO = OrderedDict((
        (1, 'rsa'),
        (2, 'dsa'),
        (3, 'ecdsa'),
        (4, 'ed25519'),
    ))

    SSHFP_HASH = OrderedDict((
        (1, 'sha1'),
        (2, 'sha256'),
    ))

    # http://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml
    TLSA_USAGE = OrderedDict((
        (0, 'pkixta'),
        (1, 'pkixee'),
        (2, 'daneta'),
        (3, 'daneee'),
    ))

    TLSA_SELECT = OrderedDict((
        (0, 'cert'),
        (1, 'spki'),
    ))

    TLSA_MATCHING = OrderedDict((
        (0, 'full'),
        (1, 'sha256'),
        (2, 'sha512'),
    ))

    SRV_PROTO = (
        'tcp',
        'udp',
        'sctp'
    )

    @staticmethod
    def validate(lookup, ref, match=None):
        if lookup in ref:
            return lookup
        elif match == 'in':
            return [code for code, name in ref.items() if lookup in name][-1]
        else:
            # OrderedDicts only!(?)
            return ref.keys()[ref.values().index(lookup)]


def _to_port(port):
    try:
        port = int(port)
        assert 1 <= port <= 65535
        return port
    except (ValueError, AssertionError):
        raise ValueError('Invalid port {0}'.format(port))


def _tree(domain, tld=False):
    '''
    Split out a domain in its parents

    Leverages tldextract to take the TLDs from publicsuffix.org
    or makes a valiant approximation of that

    :param domain: dc2.ams2.example.com
    :param tld: Include TLD in list
    :return: [ 'dc2.ams2.example.com', 'ams2.example.com', 'example.com']
    '''
    domain = domain.rstrip('.')
    assert '.' in domain, 'Provide a decent domain'

    if not tld:
        if HAS_TLDEXTRACT:
            tld = tldextract.extract(domain).suffix
        else:
            tld = re.search(r'((?:(?:ac|biz|com?|info|edu|gov|mil|name|net|n[oi]m|org)\.)?[^.]+)$', domain).group()
            log.info('Without tldextract, dns.util resolves the TLD of {0} to {1}'.format(domain, tld))

    res = [domain]
    while True:
        idx = domain.find('.')
        if idx < 0:
            break
        domain = domain[idx + 1:]
        if domain == tld:
            break
        res.append(domain)

    return res


def _weighted_order(recs):
    res = []
    weights = [rec['weight'] for rec in recs]
    while weights:
        rnd = random.random() * sum(weights)
        for i, w in enumerate(weights):
            rnd -= w
            if rnd < 0:
                res.append(recs.pop(i)['name'])
                weights.pop(i)
                break

    return res


def _cast(rec_data, rec_cast):
    if isinstance(rec_cast, dict):
        rec_data = type(rec_cast.keys()[0])(rec_data)
        res = rec_cast[rec_data]
        return res
    elif isinstance(rec_cast, (list, tuple)):
        return RFC.validate(rec_data, rec_cast)
    else:
        return rec_cast(rec_data)


def _data2rec(schema, rec_data):
    '''
    schema = OrderedDict({
        'prio': int,
        'weight': int,
        'port': to_port,
        'name': str,
    })
    rec_data = '10 20 25 myawesome.nl'

    res = {'prio': 10, 'weight': 20, 'port': 25 'name': 'myawesome.nl'}
    '''
    try:
        rec_fields = rec_data.split(' ')
        # spaces in digest fields are allowed
        assert len(rec_fields) >= len(schema)
        if len(rec_fields) > len(schema):
            cutoff = len(schema) - 1
            rec_fields = rec_fields[0:cutoff] + [''.join(rec_fields[cutoff:])]

        if len(schema) == 1:
            res = _cast(rec_fields[0], next(iter(schema.values())))
        else:
            res = dict((
                (field_name, _cast(rec_field, rec_cast))
                for (field_name, rec_cast), rec_field in zip(schema.items(), rec_fields)
            ))
        return res
    except (AssertionError, AttributeError, TypeError, ValueError) as e:
        raise ValueError('Unable to cast "{0}" as "{2}": {1}'.format(
            rec_data,
            e,
            ' '.join(schema.keys())
        ))


def _data2rec_group(schema, recs_data, group_key):
    if not isinstance(recs_data, (list, tuple)):
        recs_data = [recs_data]

    res = OrderedDict()

    try:
        for rdata in recs_data:
            rdata = _data2rec(schema, rdata)
            assert rdata and group_key in rdata

            idx = rdata.pop(group_key)
            if idx not in res:
                res[idx] = []

            if len(rdata) == 1:
                rdata = next(iter(rdata.values()))

            res[idx].append(rdata)
        return res
    except (AssertionError, ValueError) as e:
        raise ValueError('Unable to cast "{0}" as a group of "{1}": {2}'.format(
            ','.join(recs_data),
            ' '.join(schema.keys()),
            e
        ))


def _rec2data(*rdata):
    return ' '.join(rdata)


def _data_clean(data):
    data = data.strip(string.whitespace)
    if data.startswith(('"', '\'')) and data.endswith(('"', '\'')):
        return data[1:-1]
    else:
        return data


def _lookup_dig(name, rdtype, timeout=None, servers=None, secure=None):
    '''
    Use dig to lookup addresses
    :param name: Name of record to search
    :param rdtype: DNS record type
    :param timeout: server response timeout
    :param servers: [] of servers to use
    :return: [] of records or False if error
    '''
    cmd = 'dig {0} -t {1} '.format(DIG_OPTIONS, rdtype)
    if servers:
        cmd += ''.join(['@{0} '.format(srv) for srv in servers])
    if timeout is not None:
        if servers:
            timeout = int(float(timeout) / len(servers))
        else:
            timeout = int(timeout)
        cmd += '+time={0} '.format(timeout)
    if secure:
        cmd += '+dnssec +adflag '

    cmd = __salt__['cmd.run_all']('{0} {1}'.format(cmd, name), python_shell=False, output_loglevel='quiet')

    if 'ignoring invalid type' in cmd['stderr']:
        raise ValueError('Invalid DNS type {}'.format(rdtype))
    elif cmd['retcode'] != 0:
        log.warning(
            'dig returned (%s): %s',
            cmd['retcode'], cmd['stderr'].strip(string.whitespace + ';')
        )
        return False
    elif not cmd['stdout']:
        return []

    validated = False
    res = []
    for line in cmd['stdout'].splitlines():
        _, rtype, rdata = line.split(None, 2)
        if rtype == 'CNAME' and rdtype != 'CNAME':
            continue
        elif rtype == 'RRSIG':
            validated = True
            continue
        res.append(_data_clean(rdata))

    if res and secure and not validated:
        return False
    else:
        return res


def _lookup_drill(name, rdtype, timeout=None, servers=None, secure=None):
    '''
    Use drill to lookup addresses
    :param name: Name of record to search
    :param rdtype: DNS record type
    :param timeout: command return timeout
    :param servers: [] of servers to use
    :return: [] of records or False if error
    '''
    cmd = 'drill '
    if secure:
        cmd += '-D -o ad '
    cmd += '{0} {1} '.format(rdtype, name)
    if servers:
        cmd += ''.join(['@{0} '.format(srv) for srv in servers])
    cmd = __salt__['cmd.run_all'](
        cmd, timeout=timeout,
        python_shell=False, output_loglevel='quiet')

    if cmd['retcode'] != 0:
        log.warning('drill returned (%s): %s', cmd['retcode'], cmd['stderr'])
        return False

    lookup_res = iter(cmd['stdout'].splitlines())
    validated = False
    res = []
    try:
        line = ''
        while 'ANSWER SECTION' not in line:
            line = next(lookup_res)
        while True:
            line = next(lookup_res)
            line = line.strip()
            if not line or line.startswith(';;'):
                break

            l_type, l_rec = line.split(None, 4)[-2:]
            if l_type == 'CNAME' and rdtype != 'CNAME':
                continue
            elif l_type == 'RRSIG':
                validated = True
                continue
            elif l_type != rdtype:
                raise ValueError('Invalid DNS type {}'.format(rdtype))

            res.append(_data_clean(l_rec))

    except StopIteration:
        pass

    if res and secure and not validated:
        return False
    else:
        return res


def _lookup_gai(name, rdtype, timeout=None):
    '''
    Use Python's socket interface to lookup addresses
    :param name: Name of record to search
    :param rdtype: A or AAAA
    :param timeout: ignored
    :return: [] of addresses or False if error
    '''
    try:
        sock_t = {
            'A':    socket.AF_INET,
            'AAAA': socket.AF_INET6
        }[rdtype]
    except KeyError:
        raise ValueError('Invalid DNS type {} for gai lookup'.format(rdtype))

    if timeout:
        log.info('Ignoring timeout on gai resolver; fix resolv.conf to do that')

    try:
        addresses = [sock[4][0] for sock in socket.getaddrinfo(name, None, sock_t, 0, socket.SOCK_RAW)]
        return addresses
    except socket.gaierror:
        return False


def _lookup_host(name, rdtype, timeout=None, server=None):
    '''
    Use host to lookup addresses
    :param name: Name of record to search
    :param server: Server to query
    :param rdtype: DNS record type
    :param timeout: server response wait
    :return: [] of records or False if error
    '''
    cmd = 'host -t {0} '.format(rdtype)

    if timeout:
        cmd += '-W {0} '.format(int(timeout))
    cmd += name
    if server is not None:
        cmd += ' {0}'.format(server)

    cmd = __salt__['cmd.run_all'](cmd, python_shell=False, output_loglevel='quiet')

    if 'invalid type' in cmd['stderr']:
        raise ValueError('Invalid DNS type {}'.format(rdtype))
    elif cmd['retcode'] != 0:
        log.warning('host returned (%s): %s', cmd['retcode'], cmd['stderr'])
        return False
    elif 'has no' in cmd['stdout']:
        return []

    res = []
    _stdout = cmd['stdout'] if server is None else cmd['stdout'].split('\n\n')[-1]
    for line in _stdout.splitlines():
        if rdtype != 'CNAME' and 'is an alias' in line:
            continue
        line = line.split(' ', 3)[-1]
        for prefix in ('record', 'address', 'handled by', 'alias for'):
            if line.startswith(prefix):
                line = line[len(prefix) + 1:]
                break
        res.append(_data_clean(line))

    return res


def _lookup_dnspython(name, rdtype, timeout=None, servers=None, secure=None):
    '''
    Use dnspython to lookup addresses
    :param name: Name of record to search
    :param rdtype: DNS record type
    :param timeout: query timeout
    :param server: [] of server(s) to try in order
    :return: [] of records or False if error
    '''
    resolver = dns.resolver.Resolver()

    if timeout is not None:
        resolver.lifetime = float(timeout)
    if servers:
        resolver.nameservers = servers
    if secure:
        resolver.ednsflags += dns.flags.DO

    try:
        res = [_data_clean(rr.to_text())
               for rr in resolver.query(name, rdtype, raise_on_no_answer=False)]
        return res
    except dns.rdatatype.UnknownRdatatype:
        raise ValueError('Invalid DNS type {}'.format(rdtype))
    except (dns.resolver.NXDOMAIN,
            dns.resolver.YXDOMAIN,
            dns.resolver.NoNameservers,
            dns.exception.Timeout):
        return False


def _lookup_nslookup(name, rdtype, timeout=None, server=None):
    '''
    Use nslookup to lookup addresses
    :param name: Name of record to search
    :param rdtype: DNS record type
    :param timeout: server response timeout
    :param server: server to query
    :return: [] of records or False if error
    '''
    cmd = 'nslookup -query={0} {1}'.format(rdtype, name)

    if timeout is not None:
        cmd += ' -timeout={0}'.format(int(timeout))
    if server is not None:
        cmd += ' {0}'.format(server)

    cmd = __salt__['cmd.run_all'](cmd, python_shell=False, output_loglevel='quiet')

    if cmd['retcode'] != 0:
        log.warning(
            'nslookup returned (%s): %s',
            cmd['retcode'],
            cmd['stdout'].splitlines()[-1].strip(string.whitespace + ';')
        )
        return False

    lookup_res = iter(cmd['stdout'].splitlines())
    res = []
    try:
        line = next(lookup_res)
        if 'unknown query type' in line:
            raise ValueError('Invalid DNS type {}'.format(rdtype))

        while True:
            if name in line:
                break
            line = next(lookup_res)

        while True:
            line = line.strip()
            if not line or line.startswith('*'):
                break
            elif rdtype != 'CNAME' and 'canonical name' in line:
                name = line.split()[-1][:-1]
                line = next(lookup_res)
                continue
            elif rdtype == 'SOA':
                line = line.split('=')
            elif line.startswith('Name:'):
                line = next(lookup_res)
                line = line.split(':', 1)
            elif line.startswith(name):
                if '=' in line:
                    line = line.split('=', 1)
                else:
                    line = line.split(' ')

            res.append(_data_clean(line[-1]))
            line = next(lookup_res)

    except StopIteration:
        pass

    if rdtype == 'SOA':
        return [' '.join(res[1:])]
    else:
        return res


def lookup(
    name,
    rdtype,
    method=None,
    servers=None,
    timeout=None,
    walk=False,
    walk_tld=False,
    secure=None
):
    '''
    Lookup DNS records and return their data

    :param name: name to lookup
    :param rdtype: DNS record type
    :param method: gai (getaddrinfo()), dnspython, dig, drill, host, nslookup or auto (default)
    :param servers: (list of) server(s) to try in-order
    :param timeout: query timeout or a valiant approximation of that
    :param walk: Walk the DNS upwards looking for the record type or name/recordtype if walk='name'.
    :param walk_tld: Include the final domain in the walk
    :param secure: return only DNSSEC secured responses
    :return: [] of record data
    '''
    # opts = __opts__.get('dns', {})
    opts = {}
    method = method or opts.get('method', 'auto')
    secure = secure or opts.get('secure', None)
    servers = servers or opts.get('servers', None)
    timeout = timeout or opts.get('timeout', False)

    rdtype = rdtype.upper()

    # pylint: disable=bad-whitespace,multiple-spaces-before-keyword
    query_methods = (
        ('gai',       _lookup_gai,       not any((rdtype not in ('A', 'AAAA'), servers, secure))),
        ('dnspython', _lookup_dnspython, HAS_DNSPYTHON),
        ('dig',       _lookup_dig,       HAS_DIG),
        ('drill',     _lookup_drill,     HAS_DRILL),
        ('host',      _lookup_host,      HAS_HOST and not secure),
        ('nslookup',  _lookup_nslookup,  HAS_NSLOOKUP and not secure),
    )
    # pylint: enable=bad-whitespace,multiple-spaces-before-keyword

    try:
        if method == 'auto':
            # The first one not to bork on the conditions becomes the function
            method, resolver = next(((rname, rcb) for rname, rcb, rtest in query_methods if rtest))
        else:
            # The first one not to bork on the conditions becomes the function. And the name must match.
            resolver = next((rcb for rname, rcb, rtest in query_methods if rname == method and rtest))
    except StopIteration:
        log.error(
            'Unable to lookup %s/%s: Resolver method %s invalid, unsupported '
            'or unable to perform query', method, rdtype, name
        )
        return False

    res_kwargs = {
        'rdtype': rdtype,
    }

    if servers:
        if not isinstance(servers, (list, tuple)):
            servers = [servers]
        if method in ('dnspython', 'dig', 'drill'):
            res_kwargs['servers'] = servers
        else:
            if timeout:
                timeout /= len(servers)

            # Inject a wrapper for multi-server behaviour
            def _multi_srvr(resolv_func):
                @functools.wraps(resolv_func)
                def _wrapper(**res_kwargs):
                    for server in servers:
                        s_res = resolv_func(server=server, **res_kwargs)
                        if s_res:
                            return s_res
                return _wrapper
            resolver = _multi_srvr(resolver)

    if not walk:
        name = [name]
    else:
        idx = 0
        if rdtype in ('SRV', 'TLSA'):  # The only RRs I know that have 2 name components
            idx = name.find('.') + 1
        idx = name.find('.', idx) + 1
        domain = name[idx:]
        rname = name[0:idx]

        name = _tree(domain, walk_tld)
        if walk == 'name':
            name = [rname + domain for domain in name]

        if timeout:
            timeout /= len(name)

    if secure:
        res_kwargs['secure'] = secure
    if timeout:
        res_kwargs['timeout'] = timeout

    for rname in name:
        res = resolver(name=rname, **res_kwargs)
        if res:
            return res

    return res


def query(
    name,
    rdtype,
    method=None,
    servers=None,
    timeout=None,
    walk=False,
    walk_tld=False,
    secure=None
):
    '''
    Query DNS for information.
    Where `lookup()` returns record data, `query()` tries to interpret the data and return it's results

    :param name: name to lookup
    :param rdtype: DNS record type
    :param method: gai (getaddrinfo()), pydns, dig, drill, host, nslookup or auto (default)
    :param servers: (list of) server(s) to try in-order
    :param timeout: query timeout or a valiant approximation of that
    :param secure: return only DNSSEC secured response
    :param walk: Walk the DNS upwards looking for the record type or name/recordtype if walk='name'.
    :param walk_tld: Include the top-level domain in the walk
    :return: [] of records
    '''
    rdtype = rdtype.upper()
    qargs = {
        'method':   method,
        'servers':  servers,
        'timeout':  timeout,
        'walk':     walk,
        'walk_tld': walk_tld,
        'secure':   secure
    }

    if rdtype == 'PTR' and not name.endswith('arpa'):
        name = ptr_name(name)

    if rdtype == 'SPF':
        # 'SPF' has become a regular 'TXT' again
        qres = [answer for answer in lookup(name, 'TXT', **qargs) if answer.startswith('v=spf')]
        if not qres:
            qres = lookup(name, rdtype, **qargs)
    else:
        qres = lookup(name, rdtype, **qargs)

    rec_map = {
        'A':     a_rec,
        'AAAA':  aaaa_rec,
        'CAA':   caa_rec,
        'MX':    mx_rec,
        'SOA':   soa_rec,
        'SPF':   spf_rec,
        'SRV':   srv_rec,
        'SSHFP': sshfp_rec,
        'TLSA':  tlsa_rec,
    }

    if not qres or rdtype not in rec_map:
        return qres
    elif rdtype in ('A', 'AAAA', 'SSHFP', 'TLSA'):
        res = [rec_map[rdtype](res) for res in qres]
    elif rdtype in ('SOA', 'SPF'):
        res = rec_map[rdtype](qres[0])
    else:
        res = rec_map[rdtype](qres)

    return res


def host(name, ip4=True, ip6=True, **kwargs):
    '''
    Return a list of addresses for name

    ip6:
        Return IPv6 addresses
    ip4:
        Return IPv4 addresses

    the rest is passed on to lookup()
    '''
    res = {}
    if ip6:
        ip6 = lookup(name, 'AAAA', **kwargs)
        if ip6:
            res['ip6'] = ip6
    if ip4:
        ip4 = lookup(name, 'A', **kwargs)
        if ip4:
            res['ip4'] = ip4

    return res


def a_rec(rdata):
    '''
    Validate and parse DNS record data for an A record
    :param rdata: DNS record data
    :return: { 'address': ip }
    '''
    rschema = OrderedDict((
        ('address', ipaddress.IPv4Address),
    ))
    return _data2rec(rschema, rdata)


def aaaa_rec(rdata):
    '''
    Validate and parse DNS record data for an AAAA record
    :param rdata: DNS record data
    :return: { 'address': ip }
    '''
    rschema = OrderedDict((
        ('address', ipaddress.IPv6Address),
    ))
    return _data2rec(rschema, rdata)


def caa_rec(rdatas):
    '''
    Validate and parse DNS record data for a CAA record
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    rschema = OrderedDict((
        ('flags', lambda flag: ['critical'] if int(flag) > 0 else []),
        ('tag', RFC.CAA_TAGS),
        ('value', lambda val: val.strip('\',"'))
    ))

    res = _data2rec_group(rschema, rdatas, 'tag')

    for tag in ('issue', 'issuewild'):
        tag_res = res.get(tag, False)
        if not tag_res:
            continue
        for idx, val in enumerate(tag_res):
            if ';' not in val:
                continue
            val, params = val.split(';', 1)
            params = dict(param.split('=') for param in shlex.split(params))
            tag_res[idx] = {val: params}

    return res


def mx_data(target, preference=10):
    '''
    Generate MX record data
    :param target: server
    :param preference: preference number
    :return: DNS record data
    '''
    return _rec2data(int(preference), target)


def mx_rec(rdatas):
    '''
    Validate and parse DNS record data for MX record(s)
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    rschema = OrderedDict((
        ('preference', int),
        ('name', str),
    ))
    return _data2rec_group(rschema, rdatas, 'preference')


def ptr_name(rdata):
    '''
    Return PTR name of given IP
    :param rdata: IP address
    :return: PTR record name
    '''
    try:
        return ipaddress.ip_address(rdata).reverse_pointer
    except ValueError:
        log.error(
            'Unable to generate PTR record; %s is not a valid IP address',
            rdata
        )
        return False


def soa_rec(rdata):
    '''
    Validate and parse DNS record data for SOA record(s)
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    rschema = OrderedDict((
        ('mname', str),
        ('rname', str),
        ('serial', int),
        ('refresh', int),
        ('retry', int),
        ('expire', int),
        ('minimum', int),
    ))
    return _data2rec(rschema, rdata)


def spf_rec(rdata):
    '''
    Validate and parse DNS record data for SPF record(s)
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    spf_fields = rdata.split(' ')
    if not spf_fields.pop(0).startswith('v=spf'):
        raise ValueError('Not an SPF record')

    res = OrderedDict()
    mods = set()
    for mech_spec in spf_fields:
        if mech_spec.startswith(('exp', 'redirect')):
            # It's a modifier
            mod, val = mech_spec.split('=', 1)
            if mod in mods:
                raise KeyError('Modifier {0} can only appear once'.format(mod))

            mods.add(mod)
            continue

            # TODO: Should be in something intelligent like an SPF_get
            # if mod == 'exp':
            #     res[mod] = lookup(val, 'TXT', **qargs)
            #     continue
            # elif mod == 'redirect':
            #     return query(val, 'SPF', **qargs)

        mech = {}
        if mech_spec[0] in ('+', '-', '~', '?'):
            mech['qualifier'] = mech_spec[0]
            mech_spec = mech_spec[1:]

        if ':' in mech_spec:
            mech_spec, val = mech_spec.split(':', 1)
        elif '/' in mech_spec:
            idx = mech_spec.find('/')
            mech_spec = mech_spec[0:idx]
            val = mech_spec[idx:]
        else:
            val = None

        res[mech_spec] = mech
        if not val:
            continue
        elif mech_spec in ('ip4', 'ip6'):
            val = ipaddress.ip_interface(val)
            assert val.version == int(mech_spec[-1])

        mech['value'] = val

    return res


def srv_data(target, port, prio=10, weight=10):
    '''
    Generate SRV record data
    :param target:
    :param port:
    :param prio:
    :param weight:
    :return:
    '''
    return _rec2data(prio, weight, port, target)


def srv_name(svc, proto='tcp', domain=None):
    '''
    Generate SRV record name
    :param svc: ldap, 389 etc
    :param proto: tcp, udp, sctp etc.
    :param domain: name to append
    :return:
    '''
    proto = RFC.validate(proto, RFC.SRV_PROTO)
    if isinstance(svc, int) or svc.isdigit():
        svc = _to_port(svc)

    if domain:
        domain = '.' + domain
    return '_{0}._{1}{2}'.format(svc, proto, domain)


def srv_rec(rdatas):
    '''
    Validate and parse DNS record data for SRV record(s)
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    rschema = OrderedDict((
        ('prio', int),
        ('weight', int),
        ('port', _to_port),
        ('name', str),
    ))
    return _data2rec_group(rschema, rdatas, 'prio')


def sshfp_data(key_t, hash_t, pub):
    '''
    Generate an SSHFP record
    :param key_t: rsa/dsa/ecdsa/ed25519
    :param hash_t: sha1/sha256
    :param pub: the SSH public key
    '''
    key_t = RFC.validate(key_t, RFC.SSHFP_ALGO, 'in')
    hash_t = RFC.validate(hash_t, RFC.SSHFP_HASH)

    hasher = hashlib.new(hash_t)
    hasher.update(
        base64.b64decode(pub)
    )
    ssh_fp = hasher.hexdigest()

    return _rec2data(key_t, hash_t, ssh_fp)


def sshfp_rec(rdata):
    '''
    Validate and parse DNS record data for TLSA record(s)
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    rschema = OrderedDict((
        ('algorithm', RFC.SSHFP_ALGO),
        ('fp_hash', RFC.SSHFP_HASH),
        ('fingerprint', lambda val: val.lower())  # resolvers are inconsistent on this one
    ))

    return _data2rec(rschema, rdata)


def tlsa_data(pub, usage, selector, matching):
    '''
    Generate a TLSA rec
    :param pub: Pub key in PEM format
    :param usage:
    :param selector:
    :param matching:
    :return: TLSA data portion
    '''
    usage = RFC.validate(usage, RFC.TLSA_USAGE)
    selector = RFC.validate(selector, RFC.TLSA_SELECT)
    matching = RFC.validate(matching, RFC.TLSA_MATCHING)

    pub = ssl.PEM_cert_to_DER_cert(pub.strip())
    if matching == 0:
        cert_fp = binascii.b2a_hex(pub)
    else:
        hasher = hashlib.new(RFC.TLSA_MATCHING[matching])
        hasher.update(
            pub
        )
        cert_fp = hasher.hexdigest()

    return _rec2data(usage, selector, matching, cert_fp)


def tlsa_rec(rdata):
    '''
    Validate and parse DNS record data for TLSA record(s)
    :param rdata: DNS record data
    :return: dict w/fields
    '''
    rschema = OrderedDict((
        ('usage', RFC.TLSA_USAGE),
        ('selector', RFC.TLSA_SELECT),
        ('matching', RFC.TLSA_MATCHING),
        ('pub', str)
    ))

    return _data2rec(rschema, rdata)


def service(
    svc,
    proto='tcp',
    domain=None,
    walk=False,
    secure=None
):
    '''
    Find an SRV service in a domain or it's parents
    :param svc: service to find (ldap, 389, etc)
    :param proto: protocol the service talks (tcp, udp, etc)
    :param domain: domain to start search in
    :param walk: walk the parents if domain doesn't provide the service
    :param secure: only return DNSSEC-validated results
    :return: [
        [ prio1server1, prio1server2 ],
        [ prio2server1, prio2server2 ],
    ] (the servers will already be weighted according to the SRV rules)
    '''
    qres = query(srv_name(svc, proto, domain), 'SRV', walk=walk, secure=secure)
    if not qres:
        return False

    res = []
    for _, recs in qres.items():
        res.append(_weighted_order(recs))

    return res


def services(services_file='/etc/services'):
    '''
    Parse through system-known services
    :return: {
        'svc': [
          {  'port': port
             'proto': proto,
             'desc': comment
          },
        ],
    }
    '''
    res = {}
    with salt.utils.files.fopen(services_file, 'r') as svc_defs:
        for svc_def in svc_defs.readlines():
            svc_def = salt.utils.stringutils.to_unicode(svc_def.strip())
            if not svc_def or svc_def.startswith('#'):
                continue
            elif '#' in svc_def:
                svc_def, comment = svc_def.split('#', 1)
                comment = comment.strip()
            else:
                comment = None
            svc_def = svc_def.split()

            port, proto = svc_def.pop(1).split('/')
            port = int(port)

            for name in svc_def:
                svc_res = res.get(name, {})
                pp_res = svc_res.get(port, False)
                if not pp_res:
                    svc = {
                        'port':  port,
                        'proto': proto,
                    }
                    if comment:
                        svc['desc'] = comment
                    svc_res[port] = svc
                else:
                    curr_proto = pp_res['proto']
                    if isinstance(curr_proto, (list, tuple)):
                        curr_proto.append(proto)
                    else:
                        pp_res['proto'] = [curr_proto, proto]

                    curr_desc = pp_res.get('desc', False)
                    if comment:
                        if not curr_desc:
                            pp_res['desc'] = comment
                        elif comment != curr_desc:
                            pp_res['desc'] = '{0}, {1}'.format(curr_desc, comment)
                res[name] = svc_res

    for svc, data in res.items():
        if len(data) == 1:
            res[svc] = data.values().pop()
            continue
        else:
            res[svc] = list(data.values())

    return res


def parse_resolv(src='/etc/resolv.conf'):
    '''
    Parse a resolver configuration file (traditionally /etc/resolv.conf)
    '''

    nameservers = []
    ip4_nameservers = []
    ip6_nameservers = []
    search = []
    sortlist = []
    domain = ''
    options = []

    try:
        with salt.utils.files.fopen(src) as src_file:
            # pylint: disable=too-many-nested-blocks
            for line in src_file:
                line = salt.utils.stringutils.to_unicode(line).strip().split()

                try:
                    (directive, arg) = (line[0].lower(), line[1:])
                    # Drop everything after # or ; (comments)
                    arg = list(itertools.takewhile(lambda x: x[0] not in ('#', ';'), arg))
                    if directive == 'nameserver':
                        addr = arg[0]
                        try:
                            ip_addr = ipaddress.ip_address(addr)
                            version = ip_addr.version
                            ip_addr = str(ip_addr)
                            if ip_addr not in nameservers:
                                nameservers.append(ip_addr)
                            if version == 4 and ip_addr not in ip4_nameservers:
                                ip4_nameservers.append(ip_addr)
                            elif version == 6 and ip_addr not in ip6_nameservers:
                                ip6_nameservers.append(ip_addr)
                        except ValueError as exc:
                            log.error('%s: %s', src, exc)
                    elif directive == 'domain':
                        domain = arg[0]
                    elif directive == 'search':
                        search = arg
                    elif directive == 'sortlist':
                        # A sortlist is specified by IP address netmask pairs.
                        # The netmask is optional and defaults to the natural
                        # netmask of the net. The IP address and optional
                        # network pairs are separated by slashes.
                        for ip_raw in arg:
                            try:
                                ip_net = ipaddress.ip_network(ip_raw)
                            except ValueError as exc:
                                log.error('%s: %s', src, exc)
                            else:
                                if '/' not in ip_raw:
                                    # No netmask has been provided, guess
                                    # the "natural" one
                                    if ip_net.version == 4:
                                        ip_addr = six.text_type(ip_net.network_address)
                                        # pylint: disable=protected-access
                                        mask = salt.utils.network.natural_ipv4_netmask(ip_addr)
                                        ip_net = ipaddress.ip_network(
                                            '{0}{1}'.format(ip_addr, mask),
                                            strict=False
                                        )
                                    if ip_net.version == 6:
                                        # TODO
                                        pass

                                if ip_net not in sortlist:
                                    sortlist.append(ip_net)
                    elif directive == 'options':
                        # Options allows certain internal resolver variables to
                        # be modified.
                        if arg[0] not in options:
                            options.append(arg[0])
                except IndexError:
                    continue

        if domain and search:
            # The domain and search keywords are mutually exclusive.  If more
            # than one instance of these keywords is present, the last instance
            # will override.
            log.debug(
                '%s: The domain and search keywords are mutually exclusive.',
                src
            )

        return {
            'nameservers':     nameservers,
            'ip4_nameservers': ip4_nameservers,
            'ip6_nameservers': ip6_nameservers,
            'sortlist':        [ip.with_netmask for ip in sortlist],
            'domain':          domain,
            'search':          search,
            'options':         options
        }
    except IOError:
        return {}
