transaction.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
  2. import collections
  3. import dns.exception
  4. import dns.name
  5. import dns.rdataclass
  6. import dns.rdataset
  7. import dns.rdatatype
  8. import dns.rrset
  9. import dns.serial
  10. import dns.ttl
  11. class TransactionManager:
  12. def reader(self):
  13. """Begin a read-only transaction."""
  14. raise NotImplementedError # pragma: no cover
  15. def writer(self, replacement=False):
  16. """Begin a writable transaction.
  17. *replacement*, a ``bool``. If `True`, the content of the
  18. transaction completely replaces any prior content. If False,
  19. the default, then the content of the transaction updates the
  20. existing content.
  21. """
  22. raise NotImplementedError # pragma: no cover
  23. def origin_information(self):
  24. """Returns a tuple
  25. (absolute_origin, relativize, effective_origin)
  26. giving the absolute name of the default origin for any
  27. relative domain names, the "effective origin", and whether
  28. names should be relativized. The "effective origin" is the
  29. absolute origin if relativize is False, and the empty name if
  30. relativize is true. (The effective origin is provided even
  31. though it can be computed from the absolute_origin and
  32. relativize setting because it avoids a lot of code
  33. duplication.)
  34. If the returned names are `None`, then no origin information is
  35. available.
  36. This information is used by code working with transactions to
  37. allow it to coordinate relativization. The transaction code
  38. itself takes what it gets (i.e. does not change name
  39. relativity).
  40. """
  41. raise NotImplementedError # pragma: no cover
  42. def get_class(self):
  43. """The class of the transaction manager.
  44. """
  45. raise NotImplementedError # pragma: no cover
  46. def from_wire_origin(self):
  47. """Origin to use in from_wire() calls.
  48. """
  49. (absolute_origin, relativize, _) = self.origin_information()
  50. if relativize:
  51. return absolute_origin
  52. else:
  53. return None
  54. class DeleteNotExact(dns.exception.DNSException):
  55. """Existing data did not match data specified by an exact delete."""
  56. class ReadOnly(dns.exception.DNSException):
  57. """Tried to write to a read-only transaction."""
  58. class AlreadyEnded(dns.exception.DNSException):
  59. """Tried to use an already-ended transaction."""
  60. def _ensure_immutable_rdataset(rdataset):
  61. if rdataset is None or isinstance(rdataset, dns.rdataset.ImmutableRdataset):
  62. return rdataset
  63. return dns.rdataset.ImmutableRdataset(rdataset)
  64. def _ensure_immutable_node(node):
  65. if node is None or node.is_immutable():
  66. return node
  67. return dns.node.ImmutableNode(node)
  68. class Transaction:
  69. def __init__(self, manager, replacement=False, read_only=False):
  70. self.manager = manager
  71. self.replacement = replacement
  72. self.read_only = read_only
  73. self._ended = False
  74. self._check_put_rdataset = []
  75. self._check_delete_rdataset = []
  76. self._check_delete_name = []
  77. #
  78. # This is the high level API
  79. #
  80. def get(self, name, rdtype, covers=dns.rdatatype.NONE):
  81. """Return the rdataset associated with *name*, *rdtype*, and *covers*,
  82. or `None` if not found.
  83. Note that the returned rdataset is immutable.
  84. """
  85. self._check_ended()
  86. if isinstance(name, str):
  87. name = dns.name.from_text(name, None)
  88. rdtype = dns.rdatatype.RdataType.make(rdtype)
  89. rdataset = self._get_rdataset(name, rdtype, covers)
  90. return _ensure_immutable_rdataset(rdataset)
  91. def get_node(self, name):
  92. """Return the node at *name*, if any.
  93. Returns an immutable node or ``None``.
  94. """
  95. return _ensure_immutable_node(self._get_node(name))
  96. def _check_read_only(self):
  97. if self.read_only:
  98. raise ReadOnly
  99. def add(self, *args):
  100. """Add records.
  101. The arguments may be:
  102. - rrset
  103. - name, rdataset...
  104. - name, ttl, rdata...
  105. """
  106. self._check_ended()
  107. self._check_read_only()
  108. return self._add(False, args)
  109. def replace(self, *args):
  110. """Replace the existing rdataset at the name with the specified
  111. rdataset, or add the specified rdataset if there was no existing
  112. rdataset.
  113. The arguments may be:
  114. - rrset
  115. - name, rdataset...
  116. - name, ttl, rdata...
  117. Note that if you want to replace the entire node, you should do
  118. a delete of the name followed by one or more calls to add() or
  119. replace().
  120. """
  121. self._check_ended()
  122. self._check_read_only()
  123. return self._add(True, args)
  124. def delete(self, *args):
  125. """Delete records.
  126. It is not an error if some of the records are not in the existing
  127. set.
  128. The arguments may be:
  129. - rrset
  130. - name
  131. - name, rdataclass, rdatatype, [covers]
  132. - name, rdataset...
  133. - name, rdata...
  134. """
  135. self._check_ended()
  136. self._check_read_only()
  137. return self._delete(False, args)
  138. def delete_exact(self, *args):
  139. """Delete records.
  140. The arguments may be:
  141. - rrset
  142. - name
  143. - name, rdataclass, rdatatype, [covers]
  144. - name, rdataset...
  145. - name, rdata...
  146. Raises dns.transaction.DeleteNotExact if some of the records
  147. are not in the existing set.
  148. """
  149. self._check_ended()
  150. self._check_read_only()
  151. return self._delete(True, args)
  152. def name_exists(self, name):
  153. """Does the specified name exist?"""
  154. self._check_ended()
  155. if isinstance(name, str):
  156. name = dns.name.from_text(name, None)
  157. return self._name_exists(name)
  158. def update_serial(self, value=1, relative=True, name=dns.name.empty):
  159. """Update the serial number.
  160. *value*, an `int`, is an increment if *relative* is `True`, or the
  161. actual value to set if *relative* is `False`.
  162. Raises `KeyError` if there is no SOA rdataset at *name*.
  163. Raises `ValueError` if *value* is negative or if the increment is
  164. so large that it would cause the new serial to be less than the
  165. prior value.
  166. """
  167. self._check_ended()
  168. if value < 0:
  169. raise ValueError('negative update_serial() value')
  170. if isinstance(name, str):
  171. name = dns.name.from_text(name, None)
  172. rdataset = self._get_rdataset(name, dns.rdatatype.SOA,
  173. dns.rdatatype.NONE)
  174. if rdataset is None or len(rdataset) == 0:
  175. raise KeyError
  176. if relative:
  177. serial = dns.serial.Serial(rdataset[0].serial) + value
  178. else:
  179. serial = dns.serial.Serial(value)
  180. serial = serial.value # convert back to int
  181. if serial == 0:
  182. serial = 1
  183. rdata = rdataset[0].replace(serial=serial)
  184. new_rdataset = dns.rdataset.from_rdata(rdataset.ttl, rdata)
  185. self.replace(name, new_rdataset)
  186. def __iter__(self):
  187. self._check_ended()
  188. return self._iterate_rdatasets()
  189. def changed(self):
  190. """Has this transaction changed anything?
  191. For read-only transactions, the result is always `False`.
  192. For writable transactions, the result is `True` if at some time
  193. during the life of the transaction, the content was changed.
  194. """
  195. self._check_ended()
  196. return self._changed()
  197. def commit(self):
  198. """Commit the transaction.
  199. Normally transactions are used as context managers and commit
  200. or rollback automatically, but it may be done explicitly if needed.
  201. A ``dns.transaction.Ended`` exception will be raised if you try
  202. to use a transaction after it has been committed or rolled back.
  203. Raises an exception if the commit fails (in which case the transaction
  204. is also rolled back.
  205. """
  206. self._end(True)
  207. def rollback(self):
  208. """Rollback the transaction.
  209. Normally transactions are used as context managers and commit
  210. or rollback automatically, but it may be done explicitly if needed.
  211. A ``dns.transaction.AlreadyEnded`` exception will be raised if you try
  212. to use a transaction after it has been committed or rolled back.
  213. Rollback cannot otherwise fail.
  214. """
  215. self._end(False)
  216. def check_put_rdataset(self, check):
  217. """Call *check* before putting (storing) an rdataset.
  218. The function is called with the transaction, the name, and the rdataset.
  219. The check function may safely make non-mutating transaction method
  220. calls, but behavior is undefined if mutating transaction methods are
  221. called. The check function should raise an exception if it objects to
  222. the put, and otherwise should return ``None``.
  223. """
  224. self._check_put_rdataset.append(check)
  225. def check_delete_rdataset(self, check):
  226. """Call *check* before deleting an rdataset.
  227. The function is called with the transaction, the name, the rdatatype,
  228. and the covered rdatatype.
  229. The check function may safely make non-mutating transaction method
  230. calls, but behavior is undefined if mutating transaction methods are
  231. called. The check function should raise an exception if it objects to
  232. the put, and otherwise should return ``None``.
  233. """
  234. self._check_delete_rdataset.append(check)
  235. def check_delete_name(self, check):
  236. """Call *check* before putting (storing) an rdataset.
  237. The function is called with the transaction and the name.
  238. The check function may safely make non-mutating transaction method
  239. calls, but behavior is undefined if mutating transaction methods are
  240. called. The check function should raise an exception if it objects to
  241. the put, and otherwise should return ``None``.
  242. """
  243. self._check_delete_name.append(check)
  244. #
  245. # Helper methods
  246. #
  247. def _raise_if_not_empty(self, method, args):
  248. if len(args) != 0:
  249. raise TypeError(f'extra parameters to {method}')
  250. def _rdataset_from_args(self, method, deleting, args):
  251. try:
  252. arg = args.popleft()
  253. if isinstance(arg, dns.rrset.RRset):
  254. rdataset = arg.to_rdataset()
  255. elif isinstance(arg, dns.rdataset.Rdataset):
  256. rdataset = arg
  257. else:
  258. if deleting:
  259. ttl = 0
  260. else:
  261. if isinstance(arg, int):
  262. ttl = arg
  263. if ttl > dns.ttl.MAX_TTL:
  264. raise ValueError(f'{method}: TTL value too big')
  265. else:
  266. raise TypeError(f'{method}: expected a TTL')
  267. arg = args.popleft()
  268. if isinstance(arg, dns.rdata.Rdata):
  269. rdataset = dns.rdataset.from_rdata(ttl, arg)
  270. else:
  271. raise TypeError(f'{method}: expected an Rdata')
  272. return rdataset
  273. except IndexError:
  274. if deleting:
  275. return None
  276. else:
  277. # reraise
  278. raise TypeError(f'{method}: expected more arguments')
  279. def _add(self, replace, args):
  280. try:
  281. args = collections.deque(args)
  282. if replace:
  283. method = 'replace()'
  284. else:
  285. method = 'add()'
  286. arg = args.popleft()
  287. if isinstance(arg, str):
  288. arg = dns.name.from_text(arg, None)
  289. if isinstance(arg, dns.name.Name):
  290. name = arg
  291. rdataset = self._rdataset_from_args(method, False, args)
  292. elif isinstance(arg, dns.rrset.RRset):
  293. rrset = arg
  294. name = rrset.name
  295. # rrsets are also rdatasets, but they don't print the
  296. # same and can't be stored in nodes, so convert.
  297. rdataset = rrset.to_rdataset()
  298. else:
  299. raise TypeError(f'{method} requires a name or RRset ' +
  300. 'as the first argument')
  301. if rdataset.rdclass != self.manager.get_class():
  302. raise ValueError(f'{method} has objects of wrong RdataClass')
  303. if rdataset.rdtype == dns.rdatatype.SOA:
  304. (_, _, origin) = self._origin_information()
  305. if name != origin:
  306. raise ValueError(f'{method} has non-origin SOA')
  307. self._raise_if_not_empty(method, args)
  308. if not replace:
  309. existing = self._get_rdataset(name, rdataset.rdtype,
  310. rdataset.covers)
  311. if existing is not None:
  312. if isinstance(existing, dns.rdataset.ImmutableRdataset):
  313. trds = dns.rdataset.Rdataset(existing.rdclass,
  314. existing.rdtype,
  315. existing.covers)
  316. trds.update(existing)
  317. existing = trds
  318. rdataset = existing.union(rdataset)
  319. self._checked_put_rdataset(name, rdataset)
  320. except IndexError:
  321. raise TypeError(f'not enough parameters to {method}')
  322. def _delete(self, exact, args):
  323. try:
  324. args = collections.deque(args)
  325. if exact:
  326. method = 'delete_exact()'
  327. else:
  328. method = 'delete()'
  329. arg = args.popleft()
  330. if isinstance(arg, str):
  331. arg = dns.name.from_text(arg, None)
  332. if isinstance(arg, dns.name.Name):
  333. name = arg
  334. if len(args) > 0 and (isinstance(args[0], int) or
  335. isinstance(args[0], str)):
  336. # deleting by type and (optionally) covers
  337. rdtype = dns.rdatatype.RdataType.make(args.popleft())
  338. if len(args) > 0:
  339. covers = dns.rdatatype.RdataType.make(args.popleft())
  340. else:
  341. covers = dns.rdatatype.NONE
  342. self._raise_if_not_empty(method, args)
  343. existing = self._get_rdataset(name, rdtype, covers)
  344. if existing is None:
  345. if exact:
  346. raise DeleteNotExact(f'{method}: missing rdataset')
  347. else:
  348. self._delete_rdataset(name, rdtype, covers)
  349. return
  350. else:
  351. rdataset = self._rdataset_from_args(method, True, args)
  352. elif isinstance(arg, dns.rrset.RRset):
  353. rdataset = arg # rrsets are also rdatasets
  354. name = rdataset.name
  355. else:
  356. raise TypeError(f'{method} requires a name or RRset ' +
  357. 'as the first argument')
  358. self._raise_if_not_empty(method, args)
  359. if rdataset:
  360. if rdataset.rdclass != self.manager.get_class():
  361. raise ValueError(f'{method} has objects of wrong '
  362. 'RdataClass')
  363. existing = self._get_rdataset(name, rdataset.rdtype,
  364. rdataset.covers)
  365. if existing is not None:
  366. if exact:
  367. intersection = existing.intersection(rdataset)
  368. if intersection != rdataset:
  369. raise DeleteNotExact(f'{method}: missing rdatas')
  370. rdataset = existing.difference(rdataset)
  371. if len(rdataset) == 0:
  372. self._checked_delete_rdataset(name, rdataset.rdtype,
  373. rdataset.covers)
  374. else:
  375. self._checked_put_rdataset(name, rdataset)
  376. elif exact:
  377. raise DeleteNotExact(f'{method}: missing rdataset')
  378. else:
  379. if exact and not self._name_exists(name):
  380. raise DeleteNotExact(f'{method}: name not known')
  381. self._checked_delete_name(name)
  382. except IndexError:
  383. raise TypeError(f'not enough parameters to {method}')
  384. def _check_ended(self):
  385. if self._ended:
  386. raise AlreadyEnded
  387. def _end(self, commit):
  388. self._check_ended()
  389. if self._ended:
  390. raise AlreadyEnded
  391. try:
  392. self._end_transaction(commit)
  393. finally:
  394. self._ended = True
  395. def _checked_put_rdataset(self, name, rdataset):
  396. for check in self._check_put_rdataset:
  397. check(self, name, rdataset)
  398. self._put_rdataset(name, rdataset)
  399. def _checked_delete_rdataset(self, name, rdtype, covers):
  400. for check in self._check_delete_rdataset:
  401. check(self, name, rdtype, covers)
  402. self._delete_rdataset(name, rdtype, covers)
  403. def _checked_delete_name(self, name):
  404. for check in self._check_delete_name:
  405. check(self, name)
  406. self._delete_name(name)
  407. #
  408. # Transactions are context managers.
  409. #
  410. def __enter__(self):
  411. return self
  412. def __exit__(self, exc_type, exc_val, exc_tb):
  413. if not self._ended:
  414. if exc_type is None:
  415. self.commit()
  416. else:
  417. self.rollback()
  418. return False
  419. #
  420. # This is the low level API, which must be implemented by subclasses
  421. # of Transaction.
  422. #
  423. def _get_rdataset(self, name, rdtype, covers):
  424. """Return the rdataset associated with *name*, *rdtype*, and *covers*,
  425. or `None` if not found.
  426. """
  427. raise NotImplementedError # pragma: no cover
  428. def _put_rdataset(self, name, rdataset):
  429. """Store the rdataset."""
  430. raise NotImplementedError # pragma: no cover
  431. def _delete_name(self, name):
  432. """Delete all data associated with *name*.
  433. It is not an error if the name does not exist.
  434. """
  435. raise NotImplementedError # pragma: no cover
  436. def _delete_rdataset(self, name, rdtype, covers):
  437. """Delete all data associated with *name*, *rdtype*, and *covers*.
  438. It is not an error if the rdataset does not exist.
  439. """
  440. raise NotImplementedError # pragma: no cover
  441. def _name_exists(self, name):
  442. """Does name exist?
  443. Returns a bool.
  444. """
  445. raise NotImplementedError # pragma: no cover
  446. def _changed(self):
  447. """Has this transaction changed anything?"""
  448. raise NotImplementedError # pragma: no cover
  449. def _end_transaction(self, commit):
  450. """End the transaction.
  451. *commit*, a bool. If ``True``, commit the transaction, otherwise
  452. roll it back.
  453. If committing and the commit fails, then roll back and raise an
  454. exception.
  455. """
  456. raise NotImplementedError # pragma: no cover
  457. def _set_origin(self, origin):
  458. """Set the origin.
  459. This method is called when reading a possibly relativized
  460. source, and an origin setting operation occurs (e.g. $ORIGIN
  461. in a zone file).
  462. """
  463. raise NotImplementedError # pragma: no cover
  464. def _iterate_rdatasets(self):
  465. """Return an iterator that yields (name, rdataset) tuples.
  466. """
  467. raise NotImplementedError # pragma: no cover
  468. def _get_node(self, name):
  469. """Return the node at *name*, if any.
  470. Returns a node or ``None``.
  471. """
  472. raise NotImplementedError # pragma: no cover
  473. #
  474. # Low-level API with a default implementation, in case a subclass needs
  475. # to override.
  476. #
  477. def _origin_information(self):
  478. # This is only used by _add()
  479. return self.manager.origin_information()