dyn_record.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. IMPORT_ERROR = False
  89. from dyn.tm.session import DynectSession
  90. from dyn.tm.zones import Zone
  91. import dyn.tm.errors
  92. import os
  93. except ImportError as error:
  94. IMPORT_ERROR = str(error)
  95. # Each of the record types use a different method for the value.
  96. RECORD_PARAMS = {
  97. 'A' : {'value_param': 'address'},
  98. 'AAAA' : {'value_param': 'address'},
  99. 'CNAME' : {'value_param': 'cname'},
  100. 'PTR' : {'value_param': 'ptrdname'},
  101. 'TXT' : {'value_param': 'txtdata'}
  102. }
  103. # You'll notice that the value_param doesn't match the key (records_key)
  104. # in the dict returned from Dyn when doing a dyn_node.get_all_records()
  105. # This is a frustrating lookup dict to allow mapping to the RECORD_PARAMS
  106. # dict so we can lookup other values in it efficiently
  107. def get_record_type(record_key):
  108. '''Get the record type represented by the keys returned from get_any_records.'''
  109. return record_key.replace('_records', '').upper()
  110. def get_record_key(record_type):
  111. '''Get the key to look up records in the dictionary returned from get_any_records.'''
  112. return record_type.lower() + '_records'
  113. def get_any_records(module, node):
  114. '''Get any records for a given node'''
  115. # Lets get a list of the A records for the node
  116. try:
  117. records = node.get_any_records()
  118. except dyn.tm.errors.DynectGetError as error:
  119. if 'Not in zone' in str(error):
  120. # The node isn't in the zone so we'll return an empty dictionary
  121. return {}
  122. else:
  123. # An unknown error happened so we'll need to return it.
  124. module.fail_json(msg='Unable to get records',
  125. error=str(error))
  126. # Return a dictionary of the record objects
  127. return records
  128. def get_record_values(records):
  129. '''Get the record values for each record returned by get_any_records.'''
  130. # This simply returns the values from a dictionary of record objects
  131. ret_dict = {}
  132. for key in records.keys():
  133. record_type = get_record_type(key)
  134. record_value_param = RECORD_PARAMS[record_type]['value_param']
  135. ret_dict[key] = [getattr(elem, record_value_param) for elem in records[key]]
  136. return ret_dict
  137. def main():
  138. '''Ansible module for managing Dyn DNS records.'''
  139. module = AnsibleModule(
  140. argument_spec=dict(
  141. state=dict(required=True, choices=['present', 'absent', 'list']),
  142. customer_name=dict(default=os.environ.get('DYNECT_CUSTOMER_NAME', None), type='str'),
  143. user_name=dict(default=os.environ.get('DYNECT_USER_NAME', None), type='str', no_log=True),
  144. user_password=dict(default=os.environ.get('DYNECT_PASSWORD', None), type='str', no_log=True),
  145. zone=dict(required=True),
  146. record_fqdn=dict(required=False),
  147. record_type=dict(required=False, choices=[
  148. 'A', 'AAAA', 'CNAME', 'PTR', 'TXT']),
  149. record_value=dict(required=False),
  150. record_ttl=dict(required=False, default=0, type='int'),
  151. ),
  152. required_together=(
  153. ['record_fqdn', 'record_value', 'record_ttl', 'record_type']
  154. )
  155. )
  156. if IMPORT_ERROR:
  157. module.fail_json(msg="Unable to import dyn module: https://pypi.python.org/pypi/dyn",
  158. error=IMPORT_ERROR)
  159. # Start the Dyn session
  160. try:
  161. _ = DynectSession(module.params['customer_name'],
  162. module.params['user_name'],
  163. module.params['user_password'])
  164. except dyn.tm.errors.DynectAuthError as error:
  165. module.fail_json(msg='Unable to authenticate with Dyn',
  166. error=str(error))
  167. # Retrieve zone object
  168. try:
  169. dyn_zone = Zone(module.params['zone'])
  170. except dyn.tm.errors.DynectGetError as error:
  171. if 'No such zone' in str(error):
  172. module.fail_json(
  173. msg="Not a valid zone for this account",
  174. zone=module.params['zone']
  175. )
  176. else:
  177. module.fail_json(msg="Unable to retrieve zone",
  178. error=str(error))
  179. # To retrieve the node object we need to remove the zone name from the FQDN
  180. dyn_node_name = module.params['record_fqdn'].replace('.' + module.params['zone'], '')
  181. # Retrieve the zone object from dyn
  182. dyn_zone = Zone(module.params['zone'])
  183. # Retrieve the node object from dyn
  184. dyn_node = dyn_zone.get_node(node=dyn_node_name)
  185. # All states will need a list of the exiting records for the zone.
  186. dyn_node_records = get_any_records(module, dyn_node)
  187. if module.params['state'] == 'list':
  188. module.exit_json(changed=False,
  189. records=get_record_values(
  190. dyn_node_records,
  191. ))
  192. if module.params['state'] == 'present':
  193. # First get a list of existing records for the node
  194. values = get_record_values(dyn_node_records)
  195. value_key = get_record_key(module.params['record_type'])
  196. # Check to see if the record is already in place before doing anything.
  197. if (dyn_node_records and
  198. dyn_node_records[value_key][0].ttl == module.params['record_ttl'] and
  199. module.params['record_value'] in values[value_key]):
  200. module.exit_json(changed=False)
  201. # Working on the assumption that there is only one record per
  202. # node we will first delete the node if there are any records before
  203. # creating the correct record
  204. if dyn_node_records:
  205. dyn_node.delete()
  206. # Now lets create the correct node entry.
  207. dyn_zone.add_record(dyn_node_name,
  208. module.params['record_type'],
  209. module.params['record_value'],
  210. module.params['record_ttl']
  211. )
  212. # Now publish the zone since we've updated it.
  213. dyn_zone.publish()
  214. module.exit_json(changed=True,
  215. msg="Created node %s in zone %s" % (dyn_node_name, module.params['zone']))
  216. if module.params['state'] == 'absent':
  217. # If there are any records present we'll want to delete the node.
  218. if dyn_node_records:
  219. dyn_node.delete()
  220. # Publish the zone since we've modified it.
  221. dyn_zone.publish()
  222. module.exit_json(changed=True,
  223. msg="Removed node %s from zone %s" % (dyn_node_name, module.params['zone']))
  224. else:
  225. module.exit_json(changed=False)
  226. # Ansible tends to need a wild card import so we'll use it here
  227. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  228. from ansible.module_utils.basic import *
  229. if __name__ == '__main__':
  230. main()