pdns.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import requests
  2. import json
  3. from desecapi import settings
  4. from desecapi.exceptions import PdnsException
  5. headers_nslord = {
  6. 'Accept': 'application/json',
  7. 'User-Agent': 'desecapi',
  8. 'X-API-Key': settings.NSLORD_PDNS_API_TOKEN,
  9. }
  10. headers_nsmaster = {
  11. 'User-Agent': 'desecapi',
  12. 'X-API-Key': settings.NSMASTER_PDNS_API_TOKEN,
  13. }
  14. def _pdns_delete(url):
  15. # We first delete the zone from nslord, the main authoritative source of our DNS data.
  16. # However, we do not want to wait for the zone to expire on the slave ("nsmaster").
  17. # We thus issue a second delete request on nsmaster to delete the zone there immediately.
  18. r1 = requests.delete(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  19. if r1.status_code < 200 or r1.status_code >= 300:
  20. # Deletion technically does not fail if the zone didn't exist in the first place
  21. if r1.status_code == 422 and 'Could not find domain' in r1.text:
  22. pass
  23. else:
  24. raise PdnsException(r1)
  25. # Delete from nsmaster as well
  26. r2 = requests.delete(settings.NSMASTER_PDNS_API + url, headers=headers_nsmaster)
  27. if r2.status_code < 200 or r2.status_code >= 300:
  28. # Deletion technically does not fail if the zone didn't exist in the first place
  29. if r2.status_code == 422 and 'Could not find domain' in r2.text:
  30. pass
  31. else:
  32. raise PdnsException(r2)
  33. return (r1, r2)
  34. def _pdns_post(url, body):
  35. r = requests.post(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
  36. if r.status_code < 200 or r.status_code >= 300:
  37. raise PdnsException(r)
  38. return r
  39. def _pdns_patch(url, body):
  40. r = requests.patch(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
  41. if r.status_code < 200 or r.status_code >= 300:
  42. raise PdnsException(r)
  43. return r
  44. def _pdns_get(url):
  45. r = requests.get(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  46. if r.status_code < 200 or r.status_code >= 400:
  47. raise PdnsException(r)
  48. return r
  49. def _pdns_put(url):
  50. r = requests.put(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  51. if r.status_code < 200 or r.status_code >= 500:
  52. raise PdnsException(r)
  53. return r
  54. def _delete_or_replace_rrset(name, rr_type, value, ttl=60):
  55. """
  56. Return pdns API json to either replace or delete a record set, depending on whether value is empty or not.
  57. """
  58. if value:
  59. return \
  60. {
  61. "records": [
  62. {
  63. "type": rr_type,
  64. "name": name,
  65. "disabled": False,
  66. "content": value,
  67. }
  68. ],
  69. "ttl": ttl,
  70. "changetype": "REPLACE",
  71. "type": rr_type,
  72. "name": name,
  73. }
  74. else:
  75. return \
  76. {
  77. "changetype": "DELETE",
  78. "type": rr_type,
  79. "name": name
  80. }
  81. def create_zone(domain, kind='NATIVE'):
  82. """
  83. Commands pdns to create a zone with the given name.
  84. """
  85. name = domain.name
  86. if not name.endswith('.'):
  87. name += '.'
  88. payload = {
  89. "name": name,
  90. "kind": kind.upper(),
  91. "masters": [],
  92. "nameservers": [
  93. "ns1.desec.io.",
  94. "ns2.desec.io."
  95. ]
  96. }
  97. _pdns_post('/zones', payload)
  98. # Don't forget to import automatically generated RRsets (specifically, NS)
  99. domain.sync_from_pdns()
  100. def delete_zone(domain):
  101. """
  102. Commands pdns to delete a zone with the given name.
  103. """
  104. _pdns_delete('/zones/' + domain.pdns_id)
  105. def get_zone(domain):
  106. """
  107. Retrieves a JSON representation of the zone from pdns
  108. """
  109. r = _pdns_get('/zones/' + domain.pdns_id)
  110. return r.json()
  111. def get_rrsets(domain):
  112. """
  113. Retrieves a JSON representation of the RRsets in a given zone, optionally restricting to a name and RRset type
  114. """
  115. from desecapi.models import RRset
  116. from desecapi.serializers import GenericRRsetSerializer
  117. rrsets = []
  118. for rrset in get_zone(domain)['rrsets']:
  119. data = {'domain': domain.pk,
  120. 'subname': rrset['name'][:-(len(domain.name) + 2)],
  121. 'type': rrset['type'],
  122. 'records': [record['content'] for record in rrset['records']],
  123. 'ttl': rrset['ttl']}
  124. serializer = GenericRRsetSerializer(data=data)
  125. serializer.is_valid(raise_exception=True)
  126. rrsets.append(RRset(**serializer.validated_data))
  127. return rrsets
  128. def set_rrset(rrset):
  129. return set_rrsets(rrset.domain, [rrset])
  130. def set_rrsets(domain, rrsets):
  131. from desecapi.serializers import GenericRRsetSerializer
  132. rrsets = [GenericRRsetSerializer(rrset).data for rrset in rrsets]
  133. data = {'rrsets':
  134. [{'name': rrset['name'], 'type': rrset['type'], 'ttl': rrset['ttl'],
  135. 'changetype': 'REPLACE',
  136. 'records': [{'content': record, 'disabled': False}
  137. for record in rrset['records']]
  138. }
  139. for rrset in rrsets]
  140. }
  141. _pdns_patch('/zones/' + domain.pdns_id, data)
  142. def zone_exists(domain):
  143. """
  144. Returns whether pdns knows a zone with the given name.
  145. """
  146. r = _pdns_get('/zones/' + domain.pdns_id)
  147. if r.status_code == 200:
  148. return True
  149. elif r.status_code == 422 and 'Could not find domain' in r.text:
  150. return False
  151. else:
  152. raise PdnsException(r)
  153. def notify_zone(domain):
  154. """
  155. Commands pdns to notify the zone to the pdns slaves.
  156. """
  157. _pdns_put('/zones/%s/notify' % domain.pdns_id)
  158. def set_dyn_records(domain):
  159. """
  160. Commands pdns to set the A and AAAA record for the zone with the given name to the given record values.
  161. Only supports one A, one AAAA record.
  162. If a or aaaa is empty, pdns will be commanded to delete the record.
  163. """
  164. _pdns_patch('/zones/' + domain.pdns_id, {
  165. "rrsets": [
  166. _delete_or_replace_rrset(domain.name + '.', 'a', domain.arecord),
  167. _delete_or_replace_rrset(domain.name + '.', 'aaaa', domain.aaaarecord),
  168. _delete_or_replace_rrset('_acme-challenge.%s.' % domain.name, 'txt', '"%s"' % domain.acme_challenge),
  169. ]
  170. })
  171. # Don't forget to import the updated RRsets
  172. domain.sync_from_pdns()
  173. notify_zone(domain)
  174. def set_rrset_in_parent(domain, rr_type, value):
  175. """
  176. Commands pdns to set or delete a record set for the zone with the given name.
  177. If value is empty, the rrset will be deleted.
  178. """
  179. parent_id = domain.pdns_id.split('.', 1)[1]
  180. _pdns_patch('/zones/' + parent_id, {
  181. "rrsets": [
  182. _delete_or_replace_rrset(domain.name + '.', rr_type, value),
  183. ]
  184. })