1"""
2Dynamic DNS Runner
3==================
4
5.. versionadded:: 2015.8.0
6
7Runner to interact with DNS server and create/delete/update DNS records
8
9:codeauthor: Nitin Madhok <nmadhok@g.clemson.edu>
10
11"""
12
13import logging
14import os
15
16import salt.utils.files
17import salt.utils.json
18
19HAS_LIBS = False
20try:
21    import dns.query
22    import dns.update
23    import dns.tsigkeyring
24
25    HAS_LIBS = True
26except ImportError:
27    HAS_LIBS = False
28
29
30log = logging.getLogger(__name__)
31
32
33def __virtual__():
34    """
35    Check if required libs (python-dns) is installed and load runner
36    only if they are present
37    """
38    if not HAS_LIBS:
39        return False
40
41    return True
42
43
44def _get_keyring(keyfile):
45    keyring = None
46    if keyfile and os.path.isfile(os.path.expanduser(keyfile)):
47        with salt.utils.files.fopen(keyfile) as _f:
48            keyring = dns.tsigkeyring.from_text(salt.utils.json.load(_f))
49
50    return keyring
51
52
53def create(
54    zone,
55    name,
56    ttl,
57    rdtype,
58    data,
59    keyname,
60    keyfile,
61    nameserver,
62    timeout,
63    port=53,
64    keyalgorithm="hmac-md5",
65):
66    """
67    Create a DNS record. The nameserver must be an IP address and the master running
68    this runner must have create privileges on that server.
69
70    CLI Example:
71
72    .. code-block:: bash
73
74        salt-run ddns.create domain.com my-test-vm 3600 A 10.20.30.40 my-tsig-key /etc/salt/tsig.keyring 10.0.0.1 5
75    """
76    if zone in name:
77        name = name.replace(zone, "").rstrip(".")
78    fqdn = "{}.{}".format(name, zone)
79    request = dns.message.make_query(fqdn, rdtype)
80    answer = dns.query.udp(request, nameserver, timeout, port)
81
82    rdata_value = dns.rdatatype.from_text(rdtype)
83    rdata = dns.rdata.from_text(dns.rdataclass.IN, rdata_value, data)
84
85    for rrset in answer.answer:
86        if rdata in rrset.items:
87            return {
88                fqdn: "Record of type '{}' already exists with ttl of {}".format(
89                    rdtype, rrset.ttl
90                )
91            }
92
93    keyring = _get_keyring(keyfile)
94
95    dns_update = dns.update.Update(
96        zone, keyring=keyring, keyname=keyname, keyalgorithm=keyalgorithm
97    )
98    dns_update.add(name, ttl, rdata)
99
100    answer = dns.query.udp(dns_update, nameserver, timeout, port)
101    if answer.rcode() > 0:
102        return {fqdn: "Failed to create record of type '{}'".format(rdtype)}
103
104    return {fqdn: "Created record of type '{}': {} -> {}".format(rdtype, fqdn, data)}
105
106
107def update(
108    zone,
109    name,
110    ttl,
111    rdtype,
112    data,
113    keyname,
114    keyfile,
115    nameserver,
116    timeout,
117    replace=False,
118    port=53,
119    keyalgorithm="hmac-md5",
120):
121    """
122    Replace, or update a DNS record. The nameserver must be an IP address and the master running
123    this runner must have update privileges on that server.
124
125    .. note::
126
127        If ``replace`` is set to True, all records for this name and type will first be deleted and
128        then recreated. Default is ``replace=False``.
129
130    CLI Example:
131
132    .. code-block:: bash
133
134        salt-run ddns.update domain.com my-test-vm 3600 A 10.20.30.40 my-tsig-key /etc/salt/tsig.keyring 10.0.0.1 5
135    """
136    if zone in name:
137        name = name.replace(zone, "").rstrip(".")
138    fqdn = "{}.{}".format(name, zone)
139    request = dns.message.make_query(fqdn, rdtype)
140    answer = dns.query.udp(request, nameserver, timeout, port)
141    if not answer.answer:
142        return {fqdn: "No matching DNS record(s) found"}
143
144    rdata_value = dns.rdatatype.from_text(rdtype)
145    rdata = dns.rdata.from_text(dns.rdataclass.IN, rdata_value, data)
146
147    for rrset in answer.answer:
148        if rdata in rrset.items:
149            rr = rrset.items
150            if ttl == rrset.ttl:
151                if replace and (len(answer.answer) > 1 or len(rrset.items) > 1):
152                    break
153                return {
154                    fqdn: "Record of type '{}' already present with ttl of {}".format(
155                        rdtype, ttl
156                    )
157                }
158            break
159
160    keyring = _get_keyring(keyfile)
161
162    dns_update = dns.update.Update(
163        zone, keyring=keyring, keyname=keyname, keyalgorithm=keyalgorithm
164    )
165    dns_update.replace(name, ttl, rdata)
166
167    answer = dns.query.udp(dns_update, nameserver, timeout, port)
168    if answer.rcode() > 0:
169        return {fqdn: "Failed to update record of type '{}'".format(rdtype)}
170
171    return {fqdn: "Updated record of type '{}'".format(rdtype)}
172
173
174def delete(
175    zone,
176    name,
177    keyname,
178    keyfile,
179    nameserver,
180    timeout,
181    rdtype=None,
182    data=None,
183    port=53,
184    keyalgorithm="hmac-md5",
185):
186    """
187    Delete a DNS record.
188
189    CLI Example:
190
191    .. code-block:: bash
192
193        salt-run ddns.delete domain.com my-test-vm my-tsig-key /etc/salt/tsig.keyring 10.0.0.1 5 A
194    """
195    if zone in name:
196        name = name.replace(zone, "").rstrip(".")
197    fqdn = "{}.{}".format(name, zone)
198    request = dns.message.make_query(fqdn, (rdtype or "ANY"))
199
200    answer = dns.query.udp(request, nameserver, timeout, port)
201    if not answer.answer:
202        return {fqdn: "No matching DNS record(s) found"}
203
204    keyring = _get_keyring(keyfile)
205
206    dns_update = dns.update.Update(
207        zone, keyring=keyring, keyname=keyname, keyalgorithm=keyalgorithm
208    )
209
210    if rdtype:
211        rdata_value = dns.rdatatype.from_text(rdtype)
212        if data:
213            rdata = dns.rdata.from_text(dns.rdataclass.IN, rdata_value, data)
214            dns_update.delete(name, rdata)
215        else:
216            dns_update.delete(name, rdata_value)
217    else:
218        dns_update.delete(name)
219
220    answer = dns.query.udp(dns_update, nameserver, timeout, port)
221    if answer.rcode() > 0:
222        return {fqdn: "Failed to delete DNS record(s)"}
223
224    return {fqdn: "Deleted DNS record(s)"}
225
226
227def add_host(
228    zone,
229    name,
230    ttl,
231    ip,
232    keyname,
233    keyfile,
234    nameserver,
235    timeout,
236    port=53,
237    keyalgorithm="hmac-md5",
238):
239    """
240    Create both A and PTR (reverse) records for a host.
241
242    CLI Example:
243
244    .. code-block:: bash
245
246        salt-run ddns.add_host domain.com my-test-vm 3600 10.20.30.40 my-tsig-key /etc/salt/tsig.keyring 10.0.0.1 5
247    """
248    res = []
249    if zone in name:
250        name = name.replace(zone, "").rstrip(".")
251    fqdn = "{}.{}".format(name, zone)
252
253    ret = create(
254        zone,
255        name,
256        ttl,
257        "A",
258        ip,
259        keyname,
260        keyfile,
261        nameserver,
262        timeout,
263        port,
264        keyalgorithm,
265    )
266    res.append(ret[fqdn])
267
268    parts = ip.split(".")[::-1]
269    i = len(parts)
270    popped = []
271
272    # Iterate over possible reverse zones
273    while i > 1:
274        p = parts.pop(0)
275        i -= 1
276        popped.append(p)
277
278        zone = "{}.{}".format(".".join(parts), "in-addr.arpa.")
279        name = ".".join(popped)
280        rev_fqdn = "{}.{}".format(name, zone)
281        ret = create(
282            zone,
283            name,
284            ttl,
285            "PTR",
286            "{}.".format(fqdn),
287            keyname,
288            keyfile,
289            nameserver,
290            timeout,
291            port,
292            keyalgorithm,
293        )
294
295        if "Created" in ret[rev_fqdn]:
296            res.append(ret[rev_fqdn])
297            return {fqdn: res}
298
299    res.append(ret[rev_fqdn])
300
301    return {fqdn: res}
302
303
304def delete_host(
305    zone, name, keyname, keyfile, nameserver, timeout, port=53, keyalgorithm="hmac-md5"
306):
307    """
308    Delete both forward (A) and reverse (PTR) records for a host only if the
309    forward (A) record exists.
310
311    CLI Example:
312
313    .. code-block:: bash
314
315        salt-run ddns.delete_host domain.com my-test-vm my-tsig-key /etc/salt/tsig.keyring 10.0.0.1 5
316    """
317    res = []
318    if zone in name:
319        name = name.replace(zone, "").rstrip(".")
320    fqdn = "{}.{}".format(name, zone)
321    request = dns.message.make_query(fqdn, "A")
322    answer = dns.query.udp(request, nameserver, timeout, port)
323
324    try:
325        ips = [i.address for i in answer.answer[0].items]
326    except IndexError:
327        ips = []
328
329    ret = delete(
330        zone,
331        name,
332        keyname,
333        keyfile,
334        nameserver,
335        timeout,
336        port=port,
337        keyalgorithm=keyalgorithm,
338    )
339    res.append("{} of type 'A'".format(ret[fqdn]))
340
341    for ip in ips:
342        parts = ip.split(".")[::-1]
343        i = len(parts)
344        popped = []
345
346        # Iterate over possible reverse zones
347        while i > 1:
348            p = parts.pop(0)
349            i -= 1
350            popped.append(p)
351            zone = "{}.{}".format(".".join(parts), "in-addr.arpa.")
352            name = ".".join(popped)
353            rev_fqdn = "{}.{}".format(name, zone)
354            ret = delete(
355                zone,
356                name,
357                keyname,
358                keyfile,
359                nameserver,
360                timeout,
361                "PTR",
362                "{}.".format(fqdn),
363                port,
364                keyalgorithm,
365            )
366
367            if "Deleted" in ret[rev_fqdn]:
368                res.append("{} of type 'PTR'".format(ret[rev_fqdn]))
369                return {fqdn: res}
370
371        res.append(ret[rev_fqdn])
372
373    return {fqdn: res}
374