dyn_record.py 10 KB

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