dyn_record.py 9.8 KB

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