dyn_record.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #!/usr/bin/python
  2. #
  3. # (c) 2015, Russell Harrison <rharriso@redhat.com>
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. '''Ansible module to manage records in the Dyn Managed DNS service'''
  17. DOCUMENTATION = '''
  18. ---
  19. module: dyn_record
  20. version_added: "1.9"
  21. short_description: Manage records in the Dyn Managed DNS service.
  22. description:
  23. - "Manages DNS records via the REST API of the Dyn Managed DNS service. It
  24. - "handles records only; there is no manipulation of zones or account support"
  25. - "yet. See: U(https://help.dyn.com/dns-api-knowledge-base/)"
  26. options:
  27. state:
  28. description:
  29. -"Whether the record should be c(present) or c(absent). Optionally the"
  30. - "state c(list) can be used to return the current value of a record."
  31. required: true
  32. choices: [ 'present', 'absent', 'list' ]
  33. default: present
  34. customer_name:
  35. description:
  36. - "The Dyn customer name for your account. If not set the value of the"
  37. - "c(DYNECT_CUSTOMER_NAME) environment variable is used."
  38. required: false
  39. default: nil
  40. user_name:
  41. description:
  42. - "The Dyn user name to log in with. If not set the value of the"
  43. - "c(DYNECT_USER_NAME) environment variable is used."
  44. required: false
  45. default: null
  46. user_password:
  47. description:
  48. - "The Dyn user's password to log in with. If not set the value of the"
  49. - "c(DYNECT_PASSWORD) environment variable is used."
  50. required: false
  51. default: null
  52. zone:
  53. description:
  54. - "The DNS zone in which your record is located."
  55. required: true
  56. default: null
  57. record_fqdn:
  58. description:
  59. - "Fully qualified domain name of the record name to get, create, delete,"
  60. - "or update."
  61. required: true
  62. default: null
  63. record_type:
  64. description:
  65. - "Record type."
  66. required: true
  67. choices: [ 'A', 'AAAA', 'CNAME', 'PTR', 'TXT' ]
  68. default: null
  69. record_value:
  70. description:
  71. - "Record value. If record_value is not specified; no changes will be"
  72. - "made and the module will fail"
  73. required: false
  74. default: null
  75. record_ttl:
  76. description:
  77. - 'Record's "Time to live". Number of seconds the record remains cached'
  78. - 'in DNS servers or c(0) to use the default TTL for the zone.'
  79. required: false
  80. default: 0
  81. notes:
  82. - The module makes a broad assumption that there will be only one record per "node" (FQDN).
  83. - This module returns record(s) in the "result" element when 'state' is set to 'present'. This value can be be registered and used in your playbooks.
  84. requirements: [ dyn ]
  85. author: "Russell Harrison"
  86. '''
  87. try:
  88. from dyn.tm.session import DynectSession
  89. from dyn.tm.zones import get_all_zones
  90. from dyn.tm.zones import Zone
  91. import dyn.tm.errors
  92. import json
  93. import os
  94. import sys
  95. IMPORT_ERROR = False
  96. except ImportError as e:
  97. IMPORT_ERROR = str(e)
  98. # Each of the record types use a different method for the value.
  99. record_params = {
  100. 'A' : {'value_param': 'address'},
  101. 'AAAA' : {'value_param': 'address'},
  102. 'CNAME' : {'value_param': 'cname'},
  103. 'PTR' : {'value_param': 'ptrdname'},
  104. 'TXT' : {'value_param': 'txtdata'}
  105. }
  106. # You'll notice that the value_param doesn't match the key (records_key)
  107. # in the dict returned from Dyn when doing a dyn_node.get_all_records()
  108. # This is a frustrating lookup dict to allow mapping to the record_params
  109. # dict so we can lookup other values in it efficiently
  110. def add_record(module, zone, type, value_key, value, ):
  111. # Add a new record to the zone.
  112. try:
  113. getattr(self, 'add_record')
  114. except:
  115. module.fail_json(msg='Unable to add record: ')
  116. def get_record_type(record_key):
  117. return record_key.replace('_records', '').upper()
  118. def get_record_key(record_type):
  119. return record_type.lower() + '_records'
  120. def get_any_records(module, node):
  121. # Lets get a list of the A records for the node
  122. try:
  123. records = node.get_any_records()
  124. except dyn.tm.errors.DynectGetError as e:
  125. if 'Not in zone' in str(e):
  126. # The node isn't in the zone so we'll return an empty dictionary
  127. return {}
  128. else:
  129. # An unknown error happened so we'll need to return it.
  130. module.fail_json(msg='Unable to get records',
  131. error=str(e))
  132. # Return a dictionary of the record objects
  133. return records
  134. def get_record_values(records):
  135. # This simply returns the values from a dictionary of record objects
  136. ret_dict = {}
  137. for key in records.keys():
  138. record_type = get_record_type(key)
  139. record_value_param = record_params[record_type]['value_param']
  140. ret_dict[key] = [getattr(elem, record_value_param) for elem in records[key]]
  141. return ret_dict
  142. def main():
  143. module = AnsibleModule(
  144. argument_spec=dict(
  145. state=dict(required=True, choices=['present', 'absent', 'list']),
  146. customer_name=dict(default=os.environ.get('DYNECT_CUSTOMER_NAME', None), type='str'),
  147. user_name=dict(default=os.environ.get('DYNECT_USER_NAME', None), type='str', no_log=True),
  148. user_password=dict(default=os.environ.get('DYNECT_PASSWORD', None), type='str', no_log=True),
  149. zone=dict(required=True),
  150. record_fqdn=dict(required=False),
  151. record_type=dict(required=False, choices=[
  152. 'A', 'AAAA', 'CNAME', 'PTR', 'TXT']),
  153. record_value=dict(required=False),
  154. record_ttl=dict(required=False, default=0, type='int'),
  155. ),
  156. required_together=(
  157. ['record_fqdn', 'record_value', 'record_ttl', 'record_type']
  158. )
  159. )
  160. if IMPORT_ERROR:
  161. module.fail_json(msg="Unable to import dyn module: https://pypi.python.org/pypi/dyn",
  162. error=IMPORT_ERROR)
  163. # Start the Dyn session
  164. try:
  165. dyn_session = DynectSession(module.params['customer_name'],
  166. module.params['user_name'],
  167. module.params['user_password'])
  168. except dyn.tm.errors.DynectAuthError as e:
  169. module.fail_json(msg='Unable to authenticate with Dyn',
  170. error=str(e))
  171. # Retrieve zone object
  172. try:
  173. dyn_zone = Zone(module.params['zone'])
  174. except dyn.tm.errors.DynectGetError as e:
  175. if 'No such zone' in e:
  176. module.fail_json(
  177. msg="Not a valid zone for this account",
  178. zone=module.params['zone']
  179. )
  180. else:
  181. module.fail_json(msg="Unable to retrieve zone",
  182. error=str(e))
  183. # To retrieve the node object we need to remove the zone name from the FQDN
  184. dyn_node_name = module.params['record_fqdn'].replace('.' + module.params['zone'], '')
  185. # Retrieve the zone object from dyn
  186. dyn_zone = Zone(module.params['zone'])
  187. # Retrieve the node object from dyn
  188. dyn_node = dyn_zone.get_node(node=dyn_node_name)
  189. # All states will need a list of the exiting records for the zone.
  190. dyn_node_records = get_any_records(module, dyn_node)
  191. if module.params['state'] == 'list':
  192. module.exit_json(changed=False,
  193. records=get_record_values(
  194. dyn_node_records,
  195. ))
  196. if module.params['state'] == 'present':
  197. # First get a list of existing records for the node
  198. values = get_record_values(dyn_node_records)
  199. value_key = get_record_key(module.params['record_type'])
  200. # Check to see if the record is already in place before doing anything.
  201. if (dyn_node_records and
  202. dyn_node_records[value_key][0].ttl == module.params['record_ttl'] and
  203. module.params['record_value'] in values[value_key]):
  204. module.exit_json(changed=False)
  205. # Working on the assumption that there is only one record per
  206. # node we will first delete the node if there are any records before
  207. # creating the correct record
  208. if dyn_node_records:
  209. dyn_node.delete()
  210. # Now lets create the correct node entry.
  211. dyn_zone.add_record(dyn_node_name,
  212. module.params['record_type'],
  213. module.params['record_value'],
  214. module.params['record_ttl']
  215. )
  216. # Now publish the zone since we've updated it.
  217. dyn_zone.publish()
  218. module.exit_json(changed=True,
  219. msg="Created node %s in zone %s" % (dyn_node_name, module.params['zone']))
  220. if module.params['state'] == 'absent':
  221. # If there are any records present we'll want to delete the node.
  222. if dyn_node_records:
  223. dyn_node.delete()
  224. # Publish the zone since we've modified it.
  225. dyn_zone.publish()
  226. module.exit_json(changed=True,
  227. msg="Removed node %s from zone %s" % (dyn_node_name, module.params['zone']))
  228. else:
  229. module.exit_json(changed=False)
  230. from ansible.module_utils.basic import *
  231. if __name__ == '__main__':
  232. main()