pdns.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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 _delete_or_replace_rrset(name, type, value, ttl=60):
  52. """
  53. Return pdns API json to either replace or delete a record set, depending on whether value is empty or not.
  54. """
  55. if value != "":
  56. return \
  57. {
  58. "records": [
  59. {
  60. "type": type,
  61. "name": name,
  62. "disabled": False,
  63. "content": value,
  64. }
  65. ],
  66. "ttl": ttl,
  67. "changetype": "REPLACE",
  68. "type": type,
  69. "name": name,
  70. }
  71. else:
  72. return \
  73. {
  74. "changetype": "DELETE",
  75. "type": type,
  76. "name": name
  77. }
  78. def create_zone(name, kind='NATIVE'):
  79. """
  80. Commands pdns to create a zone with the given name.
  81. """
  82. payload = {
  83. "name": normalize_hostname(name),
  84. "kind": kind.upper(),
  85. "masters": [],
  86. "nameservers": [
  87. "ns1.desec.io.",
  88. "ns2.desec.io."
  89. ]
  90. }
  91. _pdns_post('/zones', payload)
  92. def delete_zone(name):
  93. """
  94. Commands pdns to delete a zone with the given name.
  95. """
  96. _pdns_delete('/zones/' + normalize_hostname(name))
  97. def zone_exists(name):
  98. """
  99. Returns whether pdns knows a zone with the given name.
  100. """
  101. reply = _pdns_get('/zones/' + normalize_hostname(name))
  102. if reply.status_code == 200:
  103. return True
  104. elif reply.status_code == 422 and 'Could not find domain' in reply.text:
  105. return False
  106. else:
  107. raise Exception(reply.text)
  108. def set_dyn_records(name, a, aaaa, acme_challenge=''):
  109. """
  110. Commands pdns to set the A and AAAA record for the zone with the given name to the given record values.
  111. Only supports one A, one AAAA record.
  112. If a or aaaa is empty, pdns will be commanded to delete the record.
  113. """
  114. name = normalize_hostname(name)
  115. _pdns_patch('/zones/' + name, {
  116. "rrsets": [
  117. _delete_or_replace_rrset(name, 'a', a),
  118. _delete_or_replace_rrset(name, 'aaaa', aaaa),
  119. _delete_or_replace_rrset('_acme-challenge.%s' % name, 'txt', '"%s"' % acme_challenge),
  120. ]
  121. })
  122. def set_rrset(zone, name, type, value):
  123. """
  124. Commands pdns to set or delete a record set for the zone with the given name.
  125. If value is empty, the rrset will be deleted.
  126. """
  127. zone = normalize_hostname(zone)
  128. name = normalize_hostname(name)
  129. _pdns_patch('/zones/' + zone, {
  130. "rrsets": [
  131. _delete_or_replace_rrset(name, type, value),
  132. ]
  133. })