dyn_record.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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: 3600
  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. # Attempting to cname www.example.com to web1.example.com
  89. - name: Update CNAME record
  90. 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. record_ttl: 7200
  97. - name: Update A record
  98. 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. example:
  131. 'cname_records'
  132. '''
  133. return record_type.lower() + '_records'
  134. def get_any_records(module, node):
  135. '''Get any records for a given node'''
  136. # Lets get a list of the A records for the node
  137. try:
  138. records = node.get_any_records()
  139. except dyn.tm.errors.DynectGetError as error:
  140. if 'Not in zone' in str(error):
  141. # The node isn't in the zone so we'll return an empty dictionary
  142. return {}
  143. else:
  144. # An unknown error happened so we'll need to return it.
  145. module.fail_json(msg='Unable to get records',
  146. error=str(error))
  147. # Return a dictionary of the record objects
  148. return records
  149. def get_record_values(records):
  150. '''Get the record values for each record returned by get_any_records.'''
  151. # This simply returns the values from a dictionary of record objects
  152. ret_dict = {}
  153. for key in records.keys():
  154. record_type = get_record_type(key)
  155. record_value_param = RECORD_PARAMS[record_type]['value_param']
  156. ret_dict[key] = [getattr(elem, record_value_param) for elem in records[key]]
  157. return ret_dict
  158. def main():
  159. '''Ansible module for managing Dyn DNS records.'''
  160. module = AnsibleModule(
  161. argument_spec=dict(
  162. state=dict(default='present', choices=['present', 'absent', 'list']),
  163. customer_name=dict(default=os.environ.get('DYNECT_CUSTOMER_NAME', None), type='str'),
  164. user_name=dict(default=os.environ.get('DYNECT_USER_NAME', None), type='str', no_log=True),
  165. user_password=dict(default=os.environ.get('DYNECT_PASSWORD', None), type='str', no_log=True),
  166. zone=dict(required=True, type='str'),
  167. record_fqdn=dict(required=False, type='str'),
  168. record_type=dict(required=False, type='str', choices=[
  169. 'A', 'AAAA', 'CNAME', 'PTR', 'TXT']),
  170. record_value=dict(required=False, type='str'),
  171. record_ttl=dict(required=False, default=3600, type='int'),
  172. ),
  173. required_together=(
  174. ['record_fqdn', 'record_value', 'record_ttl', 'record_type']
  175. )
  176. )
  177. if IMPORT_ERROR:
  178. module.fail_json(msg="Unable to import dyn module: https://pypi.python.org/pypi/dyn",
  179. error=IMPORT_ERROR)
  180. # Start the Dyn session
  181. try:
  182. _ = DynectSession(module.params['customer_name'],
  183. module.params['user_name'],
  184. module.params['user_password'])
  185. except dyn.tm.errors.DynectAuthError as error:
  186. module.fail_json(msg='Unable to authenticate with Dyn',
  187. error=str(error))
  188. # Retrieve zone object
  189. try:
  190. dyn_zone = Zone(module.params['zone'])
  191. except dyn.tm.errors.DynectGetError as error:
  192. if 'No such zone' in str(error):
  193. module.fail_json(
  194. msg="Not a valid zone for this account",
  195. zone=module.params['zone']
  196. )
  197. else:
  198. module.fail_json(msg="Unable to retrieve zone",
  199. error=str(error))
  200. # To retrieve the node object we need to remove the zone name from the FQDN
  201. dyn_node_name = module.params['record_fqdn'].replace('.' + module.params['zone'], '')
  202. # Retrieve the zone object from dyn
  203. dyn_zone = Zone(module.params['zone'])
  204. # Retrieve the node object from dyn
  205. dyn_node = dyn_zone.get_node(node=dyn_node_name)
  206. # All states will need a list of the exiting records for the zone.
  207. dyn_node_records = get_any_records(module, dyn_node)
  208. dyn_values = get_record_values(dyn_node_records)
  209. # get_record_values()
  210. if module.params['state'] == 'list':
  211. module.exit_json(changed=False, records=dyn_values)
  212. if module.params['state'] == 'present':
  213. # First get a list of existing records for the node
  214. record_type_key = get_record_key(module.params['record_type'])
  215. param_value = module.params['record_value']
  216. # Check to see if the record is already in place before doing anything.
  217. if dyn_node_records and \
  218. int(dyn_node_records[record_type_key][0].ttl) == int(module.params['record_ttl']) \
  219. and ((param_value in dyn_values[record_type_key]) or param_value + '.' in dyn_values[record_type_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. rmsg = "Created node [%s] " % dyn_node_name
  235. rmsg += "in zone: %s, " % module.params['zone']
  236. rmsg += "record_type: %s, " % module.params['record_type']
  237. rmsg += "record_value: %s, " % module.params['record_value']
  238. rmsg += "record_ttl: %s" % module.params['record_ttl']
  239. module.exit_json(changed=True, msg=rmsg)
  240. if module.params['state'] == 'absent':
  241. # If there are any records present we'll want to delete the node.
  242. if dyn_node_records:
  243. dyn_node.delete()
  244. # Publish the zone since we've modified it.
  245. dyn_zone.publish()
  246. module.exit_json(changed=True,
  247. msg="Removed node %s from zone %s" % (dyn_node_name, module.params['zone']))
  248. else:
  249. module.exit_json(changed=False)
  250. # Ansible tends to need a wild card import so we'll use it here
  251. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  252. from ansible.module_utils.basic import *
  253. if __name__ == '__main__':
  254. main()