pdns.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import json
  2. import random
  3. import socket
  4. import requests
  5. from api import settings as api_settings
  6. from desecapi.exceptions import PdnsException
  7. NSLORD = object()
  8. NSMASTER = object()
  9. settings = {
  10. NSLORD: {
  11. 'base_url': api_settings.NSLORD_PDNS_API,
  12. 'headers': {
  13. 'Accept': 'application/json',
  14. 'User-Agent': 'desecapi',
  15. 'X-API-Key': api_settings.NSLORD_PDNS_API_TOKEN,
  16. }
  17. },
  18. NSMASTER: {
  19. 'base_url': api_settings.NSMASTER_PDNS_API,
  20. 'headers': {
  21. 'Accept': 'application/json',
  22. 'User-Agent': 'desecapi',
  23. 'X-API-Key': api_settings.NSMASTER_PDNS_API_TOKEN,
  24. }
  25. }
  26. }
  27. def _pdns_delete_zone(domain):
  28. path = '/zones/' + domain.pdns_id
  29. # We first delete the zone from nslord, the main authoritative source of our DNS data.
  30. # However, we do not want to wait for the zone to expire on the slave ("nsmaster").
  31. # We thus issue a second delete request on nsmaster to delete the zone there immediately.
  32. r1 = requests.delete(settings[NSLORD]['base_url'] + path, headers=settings[NSLORD]['headers'])
  33. if r1.status_code < 200 or r1.status_code >= 300:
  34. # Deletion technically does not fail if the zone didn't exist in the first place
  35. if r1.status_code == 422 and 'Could not find domain' in r1.text:
  36. pass
  37. else:
  38. raise PdnsException(r1)
  39. # Delete from nsmaster as well
  40. r2 = requests.delete(settings[NSMASTER]['base_url'] + path, headers=settings[NSMASTER]['headers'])
  41. if r2.status_code < 200 or r2.status_code >= 300:
  42. # Deletion technically does not fail if the zone didn't exist in the first place
  43. if r2.status_code == 422 and 'Could not find domain' in r2.text:
  44. pass
  45. else:
  46. raise PdnsException(r2)
  47. return r1, r2
  48. def _pdns_request(method, *, server, path, body=None, acceptable_range=range(200, 300)):
  49. data = json.dumps(body) if body else None
  50. if data is not None and len(data) > api_settings.PDNS_MAX_BODY_SIZE:
  51. raise PdnsException(detail='Payload too large', status=413)
  52. r = requests.request(method, settings[server]['base_url'] + path, data=data, headers=settings[server]['headers'])
  53. if r.status_code not in acceptable_range:
  54. raise PdnsException(r)
  55. return r
  56. def _pdns_post(server, path, body):
  57. return _pdns_request('post', server=server, path=path, body=body)
  58. def _pdns_patch(server, path, body):
  59. return _pdns_request('patch', server=server, path=path, body=body)
  60. def _pdns_get(server, path):
  61. return _pdns_request('get', server=server, path=path, acceptable_range=range(200, 400))
  62. def _pdns_put(server, path):
  63. return _pdns_request('put', server=server, path=path, acceptable_range=range(200, 500))
  64. def create_zone(domain, nameservers):
  65. """
  66. Commands pdns to create a zone with the given name and nameservers.
  67. """
  68. name = domain.name
  69. if not name.endswith('.'):
  70. name += '.'
  71. salt = '%016x' % random.randrange(16**16)
  72. payload = {'name': name, 'kind': 'MASTER', 'dnssec': True,
  73. 'nsec3param': '1 0 127 %s' % salt, 'nameservers': nameservers}
  74. _pdns_post(NSLORD, '/zones', payload)
  75. payload = {'name': name, 'kind': 'SLAVE', 'masters': [socket.gethostbyname('nslord')]}
  76. _pdns_post(NSMASTER, '/zones', payload)
  77. axfr_zone(domain)
  78. def delete_zone(domain):
  79. """
  80. Commands pdns to delete a zone with the given name.
  81. """
  82. return _pdns_delete_zone(domain)
  83. def get_keys(domain):
  84. """
  85. Retrieves a dict representation of the DNSSEC key information
  86. """
  87. r = _pdns_get(NSLORD, '/zones/%s/cryptokeys' % domain.pdns_id)
  88. return [{k: key[k] for k in ('dnskey', 'ds', 'flags', 'keytype')}
  89. for key in r.json()
  90. if key['active'] and key['keytype'] in ['csk', 'ksk']]
  91. def get_zone(domain):
  92. """
  93. Retrieves a dict representation of the zone from pdns
  94. """
  95. r = _pdns_get(NSLORD, '/zones/' + domain.pdns_id)
  96. return r.json()
  97. def get_rrset_datas(domain):
  98. """
  99. Retrieves a dict representation of the RRsets in a given zone
  100. """
  101. return [{'domain': domain,
  102. 'subname': rrset['name'][:-(len(domain.name) + 2)],
  103. 'type': rrset['type'],
  104. 'records': [record['content'] for record in rrset['records']],
  105. 'ttl': rrset['ttl']}
  106. for rrset in get_zone(domain)['rrsets']]
  107. def set_rrsets(domain, rrsets, axfr=True):
  108. data = {
  109. 'rrsets':
  110. [
  111. {
  112. 'name': rrset.name, 'type': rrset.type, 'ttl': rrset.ttl,
  113. 'changetype': 'REPLACE',
  114. 'records': [
  115. {'content': record.content, 'disabled': False}
  116. for record in rrset.records.all()
  117. ]
  118. }
  119. for rrset in rrsets
  120. ]
  121. }
  122. _pdns_patch(NSLORD, '/zones/' + domain.pdns_id, data)
  123. if axfr:
  124. axfr_zone(domain)
  125. def axfr_zone(domain):
  126. """
  127. Commands nsmaster to retrieve the zone from nslord.
  128. """
  129. _pdns_put(NSMASTER, '/zones/%s/axfr-retrieve' % domain.pdns_id)