diff.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. # Copyright (C) 2011 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. """
  16. This helps the XFR in process with accumulating parts of diff and applying
  17. it to the datasource. It also has a 'single update mode' which is useful
  18. for DDNS.
  19. The name of the module is not yet fully decided. We might want to move it
  20. under isc.datasrc or somewhere else, because we are reusing it with DDNS.
  21. But for now, it lives here.
  22. """
  23. import isc.dns
  24. import isc.log
  25. from isc.datasrc import ZoneFinder
  26. from isc.log_messages.libxfrin_messages import *
  27. class NoSuchZone(Exception):
  28. """
  29. This is raised if a diff for non-existant zone is being created.
  30. """
  31. pass
  32. """
  33. This is the amount of changes we accumulate before calling Diff.apply
  34. automatically.
  35. The number 100 is just taken from BIND 9. We don't know the rationale
  36. for exactly this amount, but we think it is just some randomly chosen
  37. number.
  38. """
  39. # If changing this, modify the tests accordingly as well.
  40. DIFF_APPLY_TRESHOLD = 100
  41. logger = isc.log.Logger('libxfrin')
  42. class Diff:
  43. """
  44. The class represents a diff against current state of datasource on
  45. one zone. The usual way of working with it is creating it, then putting
  46. bunch of changes in and commiting at the end.
  47. If you change your mind, you can just stop using the object without
  48. really commiting it. In that case no changes will happen in the data
  49. sounce.
  50. The class works as a kind of a buffer as well, it does not direct
  51. the changes to underlying data source right away, but keeps them for
  52. a while.
  53. """
  54. def __init__(self, ds_client, zone, replace=False, journaling=False,
  55. single_update_mode=False):
  56. """
  57. Initializes the diff to a ready state. It checks the zone exists
  58. in the datasource and if not, NoSuchZone is raised. This also creates
  59. a transaction in the data source.
  60. The ds_client is the datasource client containing the zone. Zone is
  61. isc.dns.Name object representing the name of the zone (its apex).
  62. If replace is True, the content of the whole zone is wiped out before
  63. applying the diff.
  64. If journaling is True, the history of subsequent updates will be
  65. recorded as well as the updates themselves as long as the underlying
  66. data source support the journaling. If the data source allows
  67. incoming updates but does not support journaling, the Diff object
  68. will still continue applying the diffs with disabling journaling.
  69. If single_update_mode is true, the update is expected to only contain
  70. 1 set of changes (i.e. one set of additions, and one set of deletions).
  71. If so, the additions and deletions are kept separately, and applied
  72. in one go upon commit() or apply(). In this mode, additions and
  73. deletions can be done in any order. The first addition and the
  74. first deletion still have to be the new and old SOA records,
  75. respectively. Once apply() or commit() has been called, this
  76. requirement is renewed (since the diff object is essentialy reset).
  77. In this single_update_mode, upon commit, the deletions are performed
  78. first, and then the additions. With the previously mentioned
  79. restrictions, this means that the actual update looks like a single
  80. IXFR changeset (which can then be journaled). Apart from those
  81. restrictions, this class does not do any checking of data; it is
  82. the caller's responsibility to keep the data 'sane', and this class
  83. does not presume to have any knowledge of DNS zone content sanity.
  84. For instance, though it enforces the SOA to be deleted first, and
  85. added first, it does no checks on the SERIAL value.
  86. You can also expect isc.datasrc.Error or isc.datasrc.NotImplemented
  87. exceptions.
  88. """
  89. try:
  90. self.__updater = ds_client.get_updater(zone, replace, journaling)
  91. except isc.datasrc.NotImplemented as ex:
  92. if not journaling:
  93. raise ex
  94. self.__updater = ds_client.get_updater(zone, replace, False)
  95. logger.info(LIBXFRIN_NO_JOURNAL, zone, ds_client)
  96. if self.__updater is None:
  97. # The no such zone case
  98. raise NoSuchZone("Zone " + str(zone) +
  99. " does not exist in the data source " +
  100. str(ds_client))
  101. self.__single_update_mode = single_update_mode
  102. if single_update_mode:
  103. self.__additions = []
  104. self.__deletions = []
  105. else:
  106. self.__buffer = []
  107. def __check_committed(self):
  108. """
  109. This checks if the diff is already commited or broken. If it is, it
  110. raises ValueError. This check is for methods that need to work only on
  111. yet uncommited diffs.
  112. """
  113. if self.__updater is None:
  114. raise ValueError("The diff is already commited or it has raised " +
  115. "an exception, you come late")
  116. def __append_with_soa_check(self, buf, operation, rr):
  117. """
  118. Helper method for __data_common().
  119. Add the given rr to the given buffer, but with a SOA check;
  120. - if the buffer is empty, the RRType of the rr must be SOA
  121. - if the buffer is not empty, the RRType must not be SOA
  122. Raises a ValueError if these rules are not satisified.
  123. If they are, the RR is appended to the buffer.
  124. Arguments:
  125. buf: buffer to add to
  126. operation: operation to perform (either 'add' or 'delete')
  127. rr: RRset to add to the buffer
  128. """
  129. # first add or delete must be of type SOA
  130. if len(buf) == 0 and\
  131. rr.get_type() != isc.dns.RRType.SOA():
  132. raise ValueError("First " + operation +
  133. " in single update mode must be of type SOA")
  134. # And later adds or deletes may not
  135. elif len(buf) != 0 and\
  136. rr.get_type() == isc.dns.RRType.SOA():
  137. raise ValueError("Multiple SOA records in single " +
  138. "update mode " + operation)
  139. buf.append((operation, rr))
  140. def __data_common(self, rr, operation):
  141. """
  142. Schedules an operation with rr.
  143. It does all the real work of add_data and delete_data, including
  144. all checks.
  145. Raises a ValueError in several cases:
  146. - if the rrset contains multiple rrs
  147. - if the class of the rrset does not match that of the update
  148. - in single_update_mode if the first rr is not of type SOA (both
  149. for addition and deletion)
  150. - in single_update_mode if any later rr is of type SOA (both for
  151. addition and deletion)
  152. """
  153. self.__check_committed()
  154. if rr.get_rdata_count() != 1:
  155. raise ValueError('The rrset must contain exactly 1 Rdata, but ' +
  156. 'it holds ' + str(rr.get_rdata_count()))
  157. if rr.get_class() != self.__updater.get_class():
  158. raise ValueError("The rrset's class " + str(rr.get_class()) +
  159. " does not match updater's " +
  160. str(self.__updater.get_class()))
  161. if self.__single_update_mode:
  162. if operation == 'add':
  163. self.__append_with_soa_check(self.__additions, operation, rr)
  164. elif operation == 'delete':
  165. self.__append_with_soa_check(self.__deletions, operation, rr)
  166. else:
  167. self.__buffer.append((operation, rr))
  168. if len(self.__buffer) >= DIFF_APPLY_TRESHOLD:
  169. # Time to auto-apply, so the data don't accumulate too much
  170. # This is not done for DDNS type data
  171. self.apply()
  172. def add_data(self, rr):
  173. """
  174. Schedules addition of an RR into the zone in this diff.
  175. The rr is of isc.dns.RRset type and it must contain only one RR.
  176. If this is not the case or if the diff was already commited, this
  177. raises the ValueError exception.
  178. The rr class must match the one of the datasource client. If
  179. it does not, ValueError is raised.
  180. """
  181. self.__data_common(rr, 'add')
  182. def delete_data(self, rr):
  183. """
  184. Schedules deleting an RR from the zone in this diff.
  185. The rr is of isc.dns.RRset type and it must contain only one RR.
  186. If this is not the case or if the diff was already commited, this
  187. raises the ValueError exception.
  188. The rr class must match the one of the datasource client. If
  189. it does not, ValueError is raised.
  190. """
  191. self.__data_common(rr, 'delete')
  192. def compact(self):
  193. """
  194. Tries to compact the operations in buffer a little by putting some of
  195. the operations together, forming RRsets with more than one RR.
  196. This is called by apply before putting the data into datasource. You
  197. may, but not have to, call this manually.
  198. Currently it merges consecutive same operations on the same
  199. domain/type. We could do more fancy things, like sorting by the domain
  200. and do more merging, but such diffs should be rare in practice anyway,
  201. so we don't bother and do it this simple way.
  202. """
  203. def same_type(rrset1, rrset2):
  204. '''A helper routine to identify whether two RRsets are of the
  205. same 'type'. For RRSIGs we should consider type covered, too.
  206. '''
  207. if rrset1.get_type() != isc.dns.RRType.RRSIG() or \
  208. rrset2.get_type != isc.dns.RRType.RRSIG():
  209. return rrset1.get_type() == rrset2.get_type()
  210. # RR type of the both RRsets is RRSIG. Compare type covered.
  211. # We know they have exactly one RDATA.
  212. sigdata1 = rrset1.get_rdata()[0].to_text().split()[0]
  213. sigdata2 = rrset2.get_rdata()[0].to_text().split()[0]
  214. return sigdata1 == sigdata2
  215. def compact_buffer(buffer_to_compact):
  216. '''Internal helper function for compacting buffers, compacts the
  217. given buffer.
  218. Returns the compacted buffer.
  219. '''
  220. buf = []
  221. for (op, rrset) in buffer_to_compact:
  222. old = buf[-1][1] if len(buf) > 0 else None
  223. if old is None or op != buf[-1][0] or \
  224. rrset.get_name() != old.get_name() or \
  225. (not same_type(rrset, old)):
  226. buf.append((op, isc.dns.RRset(rrset.get_name(),
  227. rrset.get_class(),
  228. rrset.get_type(),
  229. rrset.get_ttl())))
  230. if rrset.get_ttl() != buf[-1][1].get_ttl():
  231. logger.warn(LIBXFRIN_DIFFERENT_TTL, rrset.get_ttl(),
  232. buf[-1][1].get_ttl(), rrset.get_name(),
  233. rrset.get_class(), rrset.get_type())
  234. for rdatum in rrset.get_rdata():
  235. buf[-1][1].add_rdata(rdatum)
  236. return buf
  237. if self.__single_update_mode:
  238. self.__additions = compact_buffer(self.__additions)
  239. self.__deletions = compact_buffer(self.__deletions)
  240. else:
  241. self.__buffer = compact_buffer(self.__buffer)
  242. def apply(self):
  243. """
  244. Push the buffered changes inside this diff down into the data source.
  245. This does not stop you from adding more changes later through this
  246. diff and it does not close the datasource transaction, so the changes
  247. will not be shown to others yet. It just means the internal memory
  248. buffer is flushed.
  249. This is called from time to time automatically, but you can call it
  250. manually if you really want to.
  251. This raises ValueError if the diff was already commited.
  252. It also can raise isc.datasrc.Error. If that happens, you should stop
  253. using this object and abort the modification.
  254. """
  255. def apply_buffer(buf):
  256. '''
  257. Helper method to apply all operations in the given buffer
  258. '''
  259. for (operation, rrset) in buf:
  260. if operation == 'add':
  261. self.__updater.add_rrset(rrset)
  262. elif operation == 'delete':
  263. self.__updater.delete_rrset(rrset)
  264. else:
  265. raise ValueError('Unknown operation ' + operation)
  266. self.__check_committed()
  267. # First, compact the data
  268. self.compact()
  269. try:
  270. # Then pass the data inside the data source
  271. if self.__single_update_mode:
  272. apply_buffer(self.__deletions)
  273. apply_buffer(self.__additions)
  274. else:
  275. apply_buffer(self.__buffer)
  276. # As everything is already in, drop the buffer
  277. except:
  278. # If there's a problem, we can't continue.
  279. self.__updater = None
  280. raise
  281. # all went well, reset state of buffers
  282. if self.__single_update_mode:
  283. self.__additions = []
  284. self.__deletions = []
  285. else:
  286. self.__buffer = []
  287. def commit(self):
  288. """
  289. Writes all the changes into the data source and makes them visible.
  290. This closes the diff, you may not use it any more. If you try to use
  291. it, you'll get ValueError.
  292. This might raise isc.datasrc.Error.
  293. """
  294. self.__check_committed()
  295. # Push the data inside the data source
  296. self.apply()
  297. # Make sure they are visible.
  298. try:
  299. self.__updater.commit()
  300. finally:
  301. # Remove the updater. That will free some resources for one, but
  302. # mark this object as already commited, so we can check
  303. # We delete it even in case the commit failed, as that makes us
  304. # unusable.
  305. self.__updater = None
  306. def get_buffer(self):
  307. """
  308. Returns the current buffer of changes not yet passed into the data
  309. source. It is in a form like [('add', rrset), ('delete', rrset),
  310. ('delete', rrset), ...].
  311. Probably useful only for testing and introspection purposes. Don't
  312. modify the list.
  313. Raises a ValueError if the buffer is in single_update_mode.
  314. """
  315. if self.__single_update_mode:
  316. raise ValueError("Compound buffer requested in single-update mode")
  317. else:
  318. return self.__buffer
  319. def get_single_update_buffers(self):
  320. """
  321. Returns the current buffers of changes not yet passed into the data
  322. source. It is a tuple of the current deletions and additions, which
  323. each are in a form like [('delete', rrset), ('delete', rrset), ...],
  324. and [('add', rrset), ('add', rrset), ..].
  325. Probably useful only for testing and introspection purposes. Don't
  326. modify the lists.
  327. Raises a ValueError if the buffer is not in single_update_mode.
  328. """
  329. if not self.__single_update_mode:
  330. raise ValueError("Separate buffers requested in single-update mode")
  331. else:
  332. return (self.__deletions, self.__additions)
  333. def find(self, name, rrtype,
  334. options=(ZoneFinder.NO_WILDCARD | ZoneFinder.FIND_GLUE_OK)):
  335. """
  336. Calls the find() method in the ZoneFinder associated with this
  337. Diff's ZoneUpdater, i.e. the find() on the zone as it was on the
  338. moment this Diff object got created.
  339. See the ZoneFinder documentation for a full description.
  340. Note that the result does not include changes made in this Diff
  341. instance so far.
  342. Options default to NO_WILDCARD and FIND_GLUE_OK.
  343. Raises a ValueError if the Diff has been committed already
  344. """
  345. self.__check_committed()
  346. return self.__updater.find(name, rrtype, options)
  347. def find_all(self, name,
  348. options=(ZoneFinder.NO_WILDCARD | ZoneFinder.FIND_GLUE_OK)):
  349. """
  350. Calls the find() method in the ZoneFinder associated with this
  351. Diff's ZoneUpdater, i.e. the find_all() on the zone as it was on the
  352. moment this Diff object got created.
  353. See the ZoneFinder documentation for a full description.
  354. Note that the result does not include changes made in this Diff
  355. instance so far.
  356. Options default to NO_WILDCARD and FIND_GLUE_OK.
  357. Raises a ValueError if the Diff has been committed already
  358. """
  359. self.__check_committed()
  360. return self.__updater.find_all(name, options)