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