1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license 2 3# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. 4# 5# Permission to use, copy, modify, and distribute this software and its 6# documentation for any purpose with or without fee is hereby granted, 7# provided that the above copyright notice and this permission notice 8# appear in all copies. 9# 10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES 11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR 13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 18"""DNS Dynamic Update Support""" 19 20 21import dns.message 22import dns.name 23import dns.opcode 24import dns.rdata 25import dns.rdataclass 26import dns.rdataset 27import dns.tsig 28 29 30class UpdateSection(dns.enum.IntEnum): 31 """Update sections""" 32 ZONE = 0 33 PREREQ = 1 34 UPDATE = 2 35 ADDITIONAL = 3 36 37 @classmethod 38 def _maximum(cls): 39 return 3 40 41globals().update(UpdateSection.__members__) 42 43 44class UpdateMessage(dns.message.Message): 45 46 _section_enum = UpdateSection 47 48 def __init__(self, zone=None, rdclass=dns.rdataclass.IN, keyring=None, 49 keyname=None, keyalgorithm=dns.tsig.default_algorithm, 50 id=None): 51 """Initialize a new DNS Update object. 52 53 See the documentation of the Message class for a complete 54 description of the keyring dictionary. 55 56 *zone*, a ``dns.name.Name``, ``str``, or ``None``, the zone 57 which is being updated. ``None`` should only be used by dnspython's 58 message constructors, as a zone is required for the convenience 59 methods like ``add()``, ``replace()``, etc. 60 61 *rdclass*, an ``int`` or ``str``, the class of the zone. 62 63 The *keyring*, *keyname*, and *keyalgorithm* parameters are passed to 64 ``use_tsig()``; see its documentation for details. 65 """ 66 super().__init__(id=id) 67 self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE) 68 if isinstance(zone, str): 69 zone = dns.name.from_text(zone) 70 self.origin = zone 71 rdclass = dns.rdataclass.RdataClass.make(rdclass) 72 self.zone_rdclass = rdclass 73 if self.origin: 74 self.find_rrset(self.zone, self.origin, rdclass, dns.rdatatype.SOA, 75 create=True, force_unique=True) 76 if keyring is not None: 77 self.use_tsig(keyring, keyname, algorithm=keyalgorithm) 78 79 @property 80 def zone(self): 81 """The zone section.""" 82 return self.sections[0] 83 84 @zone.setter 85 def zone(self, v): 86 self.sections[0] = v 87 88 @property 89 def prerequisite(self): 90 """The prerequisite section.""" 91 return self.sections[1] 92 93 @prerequisite.setter 94 def prerequisite(self, v): 95 self.sections[1] = v 96 97 @property 98 def update(self): 99 """The update section.""" 100 return self.sections[2] 101 102 @update.setter 103 def update(self, v): 104 self.sections[2] = v 105 106 def _add_rr(self, name, ttl, rd, deleting=None, section=None): 107 """Add a single RR to the update section.""" 108 109 if section is None: 110 section = self.update 111 covers = rd.covers() 112 rrset = self.find_rrset(section, name, self.zone_rdclass, rd.rdtype, 113 covers, deleting, True, True) 114 rrset.add(rd, ttl) 115 116 def _add(self, replace, section, name, *args): 117 """Add records. 118 119 *replace* is the replacement mode. If ``False``, 120 RRs are added to an existing RRset; if ``True``, the RRset 121 is replaced with the specified contents. The second 122 argument is the section to add to. The third argument 123 is always a name. The other arguments can be: 124 125 - rdataset... 126 127 - ttl, rdata... 128 129 - ttl, rdtype, string... 130 """ 131 132 if isinstance(name, str): 133 name = dns.name.from_text(name, None) 134 if isinstance(args[0], dns.rdataset.Rdataset): 135 for rds in args: 136 if replace: 137 self.delete(name, rds.rdtype) 138 for rd in rds: 139 self._add_rr(name, rds.ttl, rd, section=section) 140 else: 141 args = list(args) 142 ttl = int(args.pop(0)) 143 if isinstance(args[0], dns.rdata.Rdata): 144 if replace: 145 self.delete(name, args[0].rdtype) 146 for rd in args: 147 self._add_rr(name, ttl, rd, section=section) 148 else: 149 rdtype = dns.rdatatype.RdataType.make(args.pop(0)) 150 if replace: 151 self.delete(name, rdtype) 152 for s in args: 153 rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s, 154 self.origin) 155 self._add_rr(name, ttl, rd, section=section) 156 157 def add(self, name, *args): 158 """Add records. 159 160 The first argument is always a name. The other 161 arguments can be: 162 163 - rdataset... 164 165 - ttl, rdata... 166 167 - ttl, rdtype, string... 168 """ 169 170 self._add(False, self.update, name, *args) 171 172 def delete(self, name, *args): 173 """Delete records. 174 175 The first argument is always a name. The other 176 arguments can be: 177 178 - *empty* 179 180 - rdataset... 181 182 - rdata... 183 184 - rdtype, [string...] 185 """ 186 187 if isinstance(name, str): 188 name = dns.name.from_text(name, None) 189 if len(args) == 0: 190 self.find_rrset(self.update, name, dns.rdataclass.ANY, 191 dns.rdatatype.ANY, dns.rdatatype.NONE, 192 dns.rdatatype.ANY, True, True) 193 elif isinstance(args[0], dns.rdataset.Rdataset): 194 for rds in args: 195 for rd in rds: 196 self._add_rr(name, 0, rd, dns.rdataclass.NONE) 197 else: 198 args = list(args) 199 if isinstance(args[0], dns.rdata.Rdata): 200 for rd in args: 201 self._add_rr(name, 0, rd, dns.rdataclass.NONE) 202 else: 203 rdtype = dns.rdatatype.RdataType.make(args.pop(0)) 204 if len(args) == 0: 205 self.find_rrset(self.update, name, 206 self.zone_rdclass, rdtype, 207 dns.rdatatype.NONE, 208 dns.rdataclass.ANY, 209 True, True) 210 else: 211 for s in args: 212 rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s, 213 self.origin) 214 self._add_rr(name, 0, rd, dns.rdataclass.NONE) 215 216 def replace(self, name, *args): 217 """Replace records. 218 219 The first argument is always a name. The other 220 arguments can be: 221 222 - rdataset... 223 224 - ttl, rdata... 225 226 - ttl, rdtype, string... 227 228 Note that if you want to replace the entire node, you should do 229 a delete of the name followed by one or more calls to add. 230 """ 231 232 self._add(True, self.update, name, *args) 233 234 def present(self, name, *args): 235 """Require that an owner name (and optionally an rdata type, 236 or specific rdataset) exists as a prerequisite to the 237 execution of the update. 238 239 The first argument is always a name. 240 The other arguments can be: 241 242 - rdataset... 243 244 - rdata... 245 246 - rdtype, string... 247 """ 248 249 if isinstance(name, str): 250 name = dns.name.from_text(name, None) 251 if len(args) == 0: 252 self.find_rrset(self.prerequisite, name, 253 dns.rdataclass.ANY, dns.rdatatype.ANY, 254 dns.rdatatype.NONE, None, 255 True, True) 256 elif isinstance(args[0], dns.rdataset.Rdataset) or \ 257 isinstance(args[0], dns.rdata.Rdata) or \ 258 len(args) > 1: 259 if not isinstance(args[0], dns.rdataset.Rdataset): 260 # Add a 0 TTL 261 args = list(args) 262 args.insert(0, 0) 263 self._add(False, self.prerequisite, name, *args) 264 else: 265 rdtype = dns.rdatatype.RdataType.make(args[0]) 266 self.find_rrset(self.prerequisite, name, 267 dns.rdataclass.ANY, rdtype, 268 dns.rdatatype.NONE, None, 269 True, True) 270 271 def absent(self, name, rdtype=None): 272 """Require that an owner name (and optionally an rdata type) does 273 not exist as a prerequisite to the execution of the update.""" 274 275 if isinstance(name, str): 276 name = dns.name.from_text(name, None) 277 if rdtype is None: 278 self.find_rrset(self.prerequisite, name, 279 dns.rdataclass.NONE, dns.rdatatype.ANY, 280 dns.rdatatype.NONE, None, 281 True, True) 282 else: 283 rdtype = dns.rdatatype.RdataType.make(rdtype) 284 self.find_rrset(self.prerequisite, name, 285 dns.rdataclass.NONE, rdtype, 286 dns.rdatatype.NONE, None, 287 True, True) 288 289 def _get_one_rr_per_rrset(self, value): 290 # Updates are always one_rr_per_rrset 291 return True 292 293 def _parse_rr_header(self, section, name, rdclass, rdtype): 294 deleting = None 295 empty = False 296 if section == UpdateSection.ZONE: 297 if dns.rdataclass.is_metaclass(rdclass) or \ 298 rdtype != dns.rdatatype.SOA or \ 299 self.zone: 300 raise dns.exception.FormError 301 else: 302 if not self.zone: 303 raise dns.exception.FormError 304 if rdclass in (dns.rdataclass.ANY, dns.rdataclass.NONE): 305 deleting = rdclass 306 rdclass = self.zone[0].rdclass 307 empty = (deleting == dns.rdataclass.ANY or 308 section == UpdateSection.PREREQ) 309 return (rdclass, rdtype, deleting, empty) 310 311# backwards compatibility 312Update = UpdateMessage 313