1"""
2Support for RFC 2136 dynamic DNS updates.
3
4:depends:   - dnspython Python module
5:configuration: If you want to use TSIG authentication for the server, there
6    are a couple of optional configuration parameters made available to
7    support this (the keyname is only needed if the keyring contains more
8    than one key)::
9
10        keyfile: keyring file (default=None)
11        keyname: key name in file (default=None)
12        keyalgorithm: algorithm used to create the key
13                      (default='HMAC-MD5.SIG-ALG.REG.INT').
14            Other possible values: hmac-sha1, hmac-sha224, hmac-sha256,
15                hmac-sha384, hmac-sha512
16
17
18    The keyring file needs to be in json format and the key name needs to end
19    with an extra period in the file, similar to this:
20
21    .. code-block:: json
22
23        {"keyname.": "keycontent"}
24"""
25
26import logging
27
28import salt.utils.files
29import salt.utils.json
30
31log = logging.getLogger(__name__)
32
33try:
34    import dns.query
35    import dns.update
36    import dns.tsigkeyring
37
38    dns_support = True
39except ImportError as e:
40    dns_support = False
41
42
43def __virtual__():
44    """
45    Confirm dnspython is available.
46    """
47    if dns_support:
48        return "ddns"
49    return (
50        False,
51        "The ddns execution module cannot be loaded: dnspython not installed.",
52    )
53
54
55def _config(name, key=None, **kwargs):
56    """
57    Return a value for 'name' from command line args then config file options.
58    Specify 'key' if the config file option is not the same as 'name'.
59    """
60    if key is None:
61        key = name
62    if name in kwargs:
63        value = kwargs[name]
64    else:
65        value = __salt__["config.option"]("ddns.{}".format(key))
66        if not value:
67            value = None
68    return value
69
70
71def _get_keyring(keyfile):
72    keyring = None
73    if keyfile:
74        with salt.utils.files.fopen(keyfile) as _f:
75            keyring = dns.tsigkeyring.from_text(salt.utils.json.load(_f))
76    return keyring
77
78
79def add_host(
80    zone,
81    name,
82    ttl,
83    ip,
84    nameserver="127.0.0.1",
85    replace=True,
86    timeout=5,
87    port=53,
88    **kwargs
89):
90    """
91    Add, replace, or update the A and PTR (reverse) records for a host.
92
93    CLI Example:
94
95    .. code-block:: bash
96
97        salt ns1 ddns.add_host example.com host1 60 10.1.1.1
98    """
99    res = update(zone, name, ttl, "A", ip, nameserver, timeout, replace, port, **kwargs)
100    if res is False:
101        return False
102
103    fqdn = "{}.{}.".format(name, zone)
104    parts = ip.split(".")[::-1]
105    popped = []
106
107    # Iterate over possible reverse zones
108    while len(parts) > 1:
109        p = parts.pop(0)
110        popped.append(p)
111        zone = "{}.{}".format(".".join(parts), "in-addr.arpa.")
112        name = ".".join(popped)
113        ptr = update(
114            zone, name, ttl, "PTR", fqdn, nameserver, timeout, replace, port, **kwargs
115        )
116        if ptr:
117            return True
118    return res
119
120
121def delete_host(zone, name, nameserver="127.0.0.1", timeout=5, port=53, **kwargs):
122    """
123    Delete the forward and reverse records for a host.
124
125    Returns true if any records are deleted.
126
127    CLI Example:
128
129    .. code-block:: bash
130
131        salt ns1 ddns.delete_host example.com host1
132    """
133    fqdn = "{}.{}".format(name, zone)
134    request = dns.message.make_query(fqdn, "A")
135    answer = dns.query.udp(request, nameserver, timeout, port)
136    try:
137        ips = [i.address for i in answer.answer[0].items]
138    except IndexError:
139        ips = []
140
141    res = delete(
142        zone, name, nameserver=nameserver, timeout=timeout, port=port, **kwargs
143    )
144
145    fqdn = fqdn + "."
146    for ip in ips:
147        parts = ip.split(".")[::-1]
148        popped = []
149
150        # Iterate over possible reverse zones
151        while len(parts) > 1:
152            p = parts.pop(0)
153            popped.append(p)
154            zone = "{}.{}".format(".".join(parts), "in-addr.arpa.")
155            name = ".".join(popped)
156            ptr = delete(
157                zone,
158                name,
159                "PTR",
160                fqdn,
161                nameserver=nameserver,
162                timeout=timeout,
163                port=port,
164                **kwargs
165            )
166        if ptr:
167            res = True
168    return res
169
170
171def update(
172    zone,
173    name,
174    ttl,
175    rdtype,
176    data,
177    nameserver="127.0.0.1",
178    timeout=5,
179    replace=False,
180    port=53,
181    **kwargs
182):
183    """
184    Add, replace, or update a DNS record.
185    nameserver must be an IP address and the minion running this module
186    must have update privileges on that server.
187    If replace is true, first deletes all records for this name and type.
188
189    CLI Example:
190
191    .. code-block:: bash
192
193        salt ns1 ddns.update example.com host1 60 A 10.0.0.1
194    """
195    name = str(name)
196
197    if name[-1:] == ".":
198        fqdn = name
199    else:
200        fqdn = "{}.{}".format(name, zone)
201
202    request = dns.message.make_query(fqdn, rdtype)
203    answer = dns.query.udp(request, nameserver, timeout, port)
204
205    rdtype = dns.rdatatype.from_text(rdtype)
206    rdata = dns.rdata.from_text(dns.rdataclass.IN, rdtype, data)
207
208    keyring = _get_keyring(_config("keyfile", **kwargs))
209    keyname = _config("keyname", **kwargs)
210    keyalgorithm = _config("keyalgorithm", **kwargs) or "HMAC-MD5.SIG-ALG.REG.INT"
211
212    is_exist = False
213    for rrset in answer.answer:
214        if rdata in rrset.items:
215            if ttl == rrset.ttl:
216                if len(answer.answer) >= 1 or len(rrset.items) >= 1:
217                    is_exist = True
218                    break
219
220    dns_update = dns.update.Update(
221        zone, keyring=keyring, keyname=keyname, keyalgorithm=keyalgorithm
222    )
223    if replace:
224        dns_update.replace(name, ttl, rdata)
225    elif not is_exist:
226        dns_update.add(name, ttl, rdata)
227    else:
228        return None
229    answer = dns.query.udp(dns_update, nameserver, timeout, port)
230    if answer.rcode() > 0:
231        return False
232    return True
233
234
235def delete(
236    zone,
237    name,
238    rdtype=None,
239    data=None,
240    nameserver="127.0.0.1",
241    timeout=5,
242    port=53,
243    **kwargs
244):
245    """
246    Delete a DNS record.
247
248    CLI Example:
249
250    .. code-block:: bash
251
252        salt ns1 ddns.delete example.com host1 A
253    """
254    name = str(name)
255
256    if name[-1:] == ".":
257        fqdn = name
258    else:
259        fqdn = "{}.{}".format(name, zone)
260
261    request = dns.message.make_query(fqdn, (rdtype or "ANY"))
262    answer = dns.query.udp(request, nameserver, timeout, port)
263    if not answer.answer:
264        return None
265
266    keyring = _get_keyring(_config("keyfile", **kwargs))
267    keyname = _config("keyname", **kwargs)
268    keyalgorithm = _config("keyalgorithm", **kwargs) or "HMAC-MD5.SIG-ALG.REG.INT"
269
270    dns_update = dns.update.Update(
271        zone, keyring=keyring, keyname=keyname, keyalgorithm=keyalgorithm
272    )
273
274    if rdtype:
275        rdtype = dns.rdatatype.from_text(rdtype)
276        if data:
277            rdata = dns.rdata.from_text(dns.rdataclass.IN, rdtype, data)
278            dns_update.delete(name, rdata)
279        else:
280            dns_update.delete(name, rdtype)
281    else:
282        dns_update.delete(name)
283
284    answer = dns.query.udp(dns_update, nameserver, timeout, port)
285    if answer.rcode() > 0:
286        return False
287    return True
288