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