pdns.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. raise Exception(r1.text)
  23. # Delete from nsmaster as well
  24. r2 = requests.delete(settings.NSMASTER_PDNS_API + url, headers=headers_nsmaster)
  25. if r2.status_code < 200 or r2.status_code >= 300:
  26. # Allow this to fail if nsmaster does not know the zone yet
  27. if r2.status_code == 422 and 'Could not find domain' in r2.text:
  28. pass
  29. else:
  30. raise Exception(r2.text)
  31. return (r1, r2)
  32. def _pdns_post(url, body):
  33. r = requests.post(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
  34. if r.status_code < 200 or r.status_code >= 300:
  35. raise Exception(r.text)
  36. return r
  37. def _pdns_patch(url, body):
  38. r = requests.patch(settings.NSLORD_PDNS_API + url, data=json.dumps(body), headers=headers_nslord)
  39. if r.status_code < 200 or r.status_code >= 300:
  40. raise Exception(r.text)
  41. return r
  42. def _pdns_get(url):
  43. r = requests.get(settings.NSLORD_PDNS_API + url, headers=headers_nslord)
  44. if (r.status_code < 200 or r.status_code >= 500):
  45. raise Exception(r.text)
  46. return r
  47. def _delete_or_replace_rrset(name, type, value, ttl=60):
  48. """
  49. Return pdns API json to either replace or delete a record set, depending on whether value is empty or not.
  50. """
  51. if value != "":
  52. return \
  53. {
  54. "records": [
  55. {
  56. "type": type,
  57. "name": name,
  58. "disabled": False,
  59. "content": value,
  60. }
  61. ],
  62. "ttl": ttl,
  63. "changetype": "REPLACE",
  64. "type": type,
  65. "name": name,
  66. }
  67. else:
  68. return \
  69. {
  70. "changetype": "DELETE",
  71. "type": type,
  72. "name": name
  73. }
  74. def create_zone(name, kind='NATIVE'):
  75. """
  76. Commands pdns to create a zone with the given name.
  77. """
  78. payload = {
  79. "name": normalize_hostname(name),
  80. "kind": kind.upper(),
  81. "masters": [],
  82. "nameservers": [
  83. "ns1.desec.io.",
  84. "ns2.desec.io."
  85. ]
  86. }
  87. _pdns_post('/zones', payload)
  88. def delete_zone(name):
  89. """
  90. Commands pdns to delete a zone with the given name.
  91. """
  92. _pdns_delete('/zones/' + normalize_hostname(name))
  93. def zone_exists(name):
  94. """
  95. Returns whether pdns knows a zone with the given name.
  96. """
  97. reply = _pdns_get('/zones/' + normalize_hostname(name))
  98. if reply.status_code == 200:
  99. return True
  100. elif reply.status_code == 422 and 'Could not find domain' in reply.text:
  101. return False
  102. else:
  103. raise Exception(reply.text)
  104. def set_dyn_records(name, a, aaaa):
  105. """
  106. Commands pdns to set the A and AAAA record for the zone with the given name to the given record values.
  107. Only supports one A, one AAAA record.
  108. If a or aaaa is empty, pdns will be commanded to delete the record.
  109. """
  110. name = normalize_hostname(name)
  111. _pdns_patch('/zones/' + name, {
  112. "rrsets": [
  113. _delete_or_replace_rrset(name, 'a', a),
  114. _delete_or_replace_rrset(name, 'aaaa', aaaa),
  115. ]
  116. })
  117. def set_rrset(zone, name, type, value):
  118. """
  119. Commands pdns to set or delete a record set for the zone with the given name.
  120. If value is empty, the rrset will be deleted.
  121. """
  122. zone = normalize_hostname(zone)
  123. name = normalize_hostname(name)
  124. _pdns_patch('/zones/' + zone, {
  125. "rrsets": [
  126. _delete_or_replace_rrset(name, type, value),
  127. ]
  128. })