pdns.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import requests
  2. import json
  3. from desecapi import settings
  4. headers_nslord = {
  5. 'User-Agent': 'desecapi',
  6. 'X-API-Key': settings.NSLORD_PDNS_API_TOKEN,
  7. }
  8. headers_nsmaster = {
  9. 'User-Agent': 'desecapi',
  10. 'X-API-Key': settings.NSMASTER_PDNS_API_TOKEN,
  11. }
  12. def normalize_hostname(name):
  13. if '/' in name or '?' in name:
  14. raise Exception('Invalid hostname ' + name)
  15. return name if name.endswith('.') else name + '.'
  16. def _pdns_delete(url):
  17. # We first delete the zone from nslord, the main authoritative source of our DNS data.
  18. # However, we do not want to wait for the zone to expire on the slave ("nsmaster").
  19. # We thus issue a second delete request on nsmaster to delete the zone there immediately.
  20. r1 = requests.delete(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  21. if r1.status_code < 200 or r1.status_code >= 300:
  22. # Deletion technically does not fail if the zone didn't exist in the first place
  23. if r1.status_code == 422 and 'Could not find domain' in r1.text:
  24. pass
  25. else:
  26. raise Exception(r1.text)
  27. # Delete from nsmaster as well
  28. r2 = requests.delete(settings.NSMASTER_PDNS_API + url, headers=headers_nsmaster)
  29. if r2.status_code < 200 or r2.status_code >= 300:
  30. # Deletion technically does not fail if the zone didn't exist in the first place
  31. if r2.status_code == 422 and 'Could not find domain' in r2.text:
  32. pass
  33. else:
  34. raise Exception(r2.text)
  35. return (r1, r2)
  36. def _pdns_post(url, body):
  37. r = requests.post(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
  38. if r.status_code < 200 or r.status_code >= 300:
  39. raise Exception(r.text)
  40. return r
  41. def _pdns_patch(url, body):
  42. r = requests.patch(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
  43. if r.status_code < 200 or r.status_code >= 300:
  44. raise Exception(r.text)
  45. return r
  46. def _pdns_get(url):
  47. r = requests.get(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  48. if r.status_code < 200 or r.status_code >= 500:
  49. raise Exception(r.text)
  50. return r
  51. def _pdns_put(url):
  52. r = requests.put(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  53. if r.status_code < 200 or r.status_code >= 500:
  54. raise Exception(r.text)
  55. return r
  56. def _delete_or_replace_rrset(name, type, value, ttl=60):
  57. """
  58. Return pdns API json to either replace or delete a record set, depending on whether value is empty or not.
  59. """
  60. if value != "":
  61. return \
  62. {
  63. "records": [
  64. {
  65. "type": type,
  66. "name": name,
  67. "disabled": False,
  68. "content": value,
  69. }
  70. ],
  71. "ttl": ttl,
  72. "changetype": "REPLACE",
  73. "type": type,
  74. "name": name,
  75. }
  76. else:
  77. return \
  78. {
  79. "changetype": "DELETE",
  80. "type": type,
  81. "name": name
  82. }
  83. def create_zone(name, kind='NATIVE'):
  84. """
  85. Commands pdns to create a zone with the given name.
  86. """
  87. payload = {
  88. "name": normalize_hostname(name),
  89. "kind": kind.upper(),
  90. "masters": [],
  91. "nameservers": [
  92. "ns1.desec.io.",
  93. "ns2.desec.io."
  94. ]
  95. }
  96. _pdns_post('/zones', payload)
  97. def delete_zone(name):
  98. """
  99. Commands pdns to delete a zone with the given name.
  100. """
  101. _pdns_delete('/zones/' + normalize_hostname(name))
  102. def zone_exists(name):
  103. """
  104. Returns whether pdns knows a zone with the given name.
  105. """
  106. reply = _pdns_get('/zones/' + normalize_hostname(name))
  107. if reply.status_code == 200:
  108. return True
  109. elif reply.status_code == 422 and 'Could not find domain' in reply.text:
  110. return False
  111. else:
  112. raise Exception(reply.text)
  113. def notify_zone(name):
  114. """
  115. Commands pdns to notify the zone to the pdns slaves.
  116. """
  117. return _pdns_put('/zones/%s/notify' % normalize_hostname(name))
  118. def set_dyn_records(name, a, aaaa, acme_challenge=''):
  119. """
  120. Commands pdns to set the A and AAAA record for the zone with the given name to the given record values.
  121. Only supports one A, one AAAA record.
  122. If a or aaaa is empty, pdns will be commanded to delete the record.
  123. """
  124. name = normalize_hostname(name)
  125. _pdns_patch('/zones/' + name, {
  126. "rrsets": [
  127. _delete_or_replace_rrset(name, 'a', a),
  128. _delete_or_replace_rrset(name, 'aaaa', aaaa),
  129. _delete_or_replace_rrset('_acme-challenge.%s' % name, 'txt', '"%s"' % acme_challenge),
  130. ]
  131. })
  132. notify_zone(name)
  133. def set_rrset(zone, name, type, value):
  134. """
  135. Commands pdns to set or delete a record set for the zone with the given name.
  136. If value is empty, the rrset will be deleted.
  137. """
  138. zone = normalize_hostname(zone)
  139. name = normalize_hostname(name)
  140. _pdns_patch('/zones/' + zone, {
  141. "rrsets": [
  142. _delete_or_replace_rrset(name, type, value),
  143. ]
  144. })