summaryrefslogtreecommitdiffstats
path: root/roles/lib_dyn/library/dyn_record.py
blob: 7b80064f49884c47a5b43bf7d205496d8833f749 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
#!/usr/bin/python
#
# (c) 2015, Russell Harrison <rharriso@redhat.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''Ansible module to manage records in the Dyn Managed DNS service'''
DOCUMENTATION = '''
---
module: dyn_record
version_added: "1.9"
short_description: Manage records in the Dyn Managed DNS service.
description:
  - "Manages DNS records via the REST API of the Dyn Managed DNS service.  It
  - "handles records only; there is no manipulation of zones or account support"
  - "yet. See: U(https://help.dyn.com/dns-api-knowledge-base/)"
options:
  state:
    description:
      -"Whether the record should be c(present) or c(absent). Optionally the"
      - "state c(list) can be used to return the current value of a record."
    required: true
    choices: [ 'present', 'absent', 'list' ]
    default: present

  customer_name:
    description:
      - "The Dyn customer name for your account.  If not set the value of the"
      - "c(DYNECT_CUSTOMER_NAME) environment variable is used."
    required: false
    default: nil

  user_name:
    description:
      - "The Dyn user name to log in with. If not set the value of the"
      - "c(DYNECT_USER_NAME) environment variable is used."
    required: false
    default: null

  user_password:
    description:
      - "The Dyn user's password to log in with. If not set the value of the"
      - "c(DYNECT_PASSWORD) environment variable is used."
    required: false
    default: null

  zone:
    description:
      - "The DNS zone in which your record is located."
    required: true
    default: null

  record_fqdn:
    description:
      - "Fully qualified domain name of the record name to get, create, delete,"
      - "or update."
    required: true
    default: null

  record_type:
    description:
      - "Record type."
    required: true
    choices: [ 'A', 'AAAA', 'CNAME', 'PTR', 'TXT' ]
    default: null

  record_value:
    description:
      - "Record value. If record_value is not specified; no changes will be"
      - "made and the module will fail"
    required: false
    default: null

  record_ttl:
    description:
      - 'Record's "Time to live".  Number of seconds the record remains cached'
      - 'in DNS servers or c(0) to use the default TTL for the zone.'
    required: false
    default: 0

notes:
  - The module makes a broad assumption that there will be only one record per "node" (FQDN).
  - 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.

requirements: [ dyn ]
author: "Russell Harrison"
'''

EXAMPLES = '''
- name: Update CNAME record
  local_action:
    module: dyn_record
    state: present
    record_fqdn: www.example.com
    zone: example.com
    record_type: CNAME
    record_value: web1.example.com

- name: Update A record
  local_action:
    module: dyn_record
    state: present
    record_fqdn: web1.example.com
    zone: example.com
    record_value: 10.0.0.10
    record_type: A
'''

try:
    IMPORT_ERROR = False
    from dyn.tm.session import DynectSession
    from dyn.tm.zones import Zone
    import dyn.tm.errors
    import os

except ImportError as error:
    IMPORT_ERROR = str(error)

# Each of the record types use a different method for the value.
RECORD_PARAMS = {
    'A'     : {'value_param': 'address'},
    'AAAA'  : {'value_param': 'address'},
    'CNAME' : {'value_param': 'cname'},
    'PTR'   : {'value_param': 'ptrdname'},
    'TXT'   : {'value_param': 'txtdata'}
}

# You'll notice that the value_param doesn't match the key (records_key)
# in the dict returned from Dyn when doing a dyn_node.get_all_records()
# This is a frustrating lookup dict to allow mapping to the RECORD_PARAMS
# dict so we can lookup other values in it efficiently

def get_record_type(record_key):
    '''Get the record type represented by the keys returned from get_any_records.'''
    return record_key.replace('_records', '').upper()

def get_record_key(record_type):
    '''Get the key to look up records in the dictionary returned from get_any_records.'''
    return record_type.lower() + '_records'

def get_any_records(module, node):
    '''Get any records for a given node'''
    # Lets get a list of the A records for the node
    try:
        records = node.get_any_records()
    except dyn.tm.errors.DynectGetError as error:
        if 'Not in zone' in str(error):
            # The node isn't in the zone so we'll return an empty dictionary
            return {}
        else:
            # An unknown error happened so we'll need to return it.
            module.fail_json(msg='Unable to get records',
                             error=str(error))

    # Return a dictionary of the record objects
    return records

def get_record_values(records):
    '''Get the record values for each record returned by get_any_records.'''
    # This simply returns the values from a dictionary of record objects
    ret_dict = {}
    for key in records.keys():
        record_type = get_record_type(key)
        record_value_param = RECORD_PARAMS[record_type]['value_param']
        ret_dict[key] = [getattr(elem, record_value_param) for elem in records[key]]
    return ret_dict

def main():
    '''Ansible module for managing Dyn DNS records.'''
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(default='present', choices=['present', 'absent', 'list']),
            customer_name=dict(default=os.environ.get('DYNECT_CUSTOMER_NAME', None), type='str'),
            user_name=dict(default=os.environ.get('DYNECT_USER_NAME', None), type='str', no_log=True),
            user_password=dict(default=os.environ.get('DYNECT_PASSWORD', None), type='str', no_log=True),
            zone=dict(required=True, type='str'),
            record_fqdn=dict(required=False, type='str'),
            record_type=dict(required=False, type='str', choices=[
                'A', 'AAAA', 'CNAME', 'PTR', 'TXT']),
            record_value=dict(required=False, type='str'),
            record_ttl=dict(required=False, default=0, type='int'),
        ),
        required_together=(
            ['record_fqdn', 'record_value', 'record_ttl', 'record_type']
        )
    )

    if IMPORT_ERROR:
        module.fail_json(msg="Unable to import dyn module: https://pypi.python.org/pypi/dyn",
                         error=IMPORT_ERROR)

    # Start the Dyn session
    try:
        _ = DynectSession(module.params['customer_name'],
                          module.params['user_name'],
                          module.params['user_password'])
    except dyn.tm.errors.DynectAuthError as error:
        module.fail_json(msg='Unable to authenticate with Dyn',
                         error=str(error))

    # Retrieve zone object
    try:
        dyn_zone = Zone(module.params['zone'])
    except dyn.tm.errors.DynectGetError as error:
        if 'No such zone' in str(error):
            module.fail_json(
                msg="Not a valid zone for this account",
                zone=module.params['zone']
            )
        else:
            module.fail_json(msg="Unable to retrieve zone",
                             error=str(error))


    # To retrieve the node object we need to remove the zone name from the FQDN
    dyn_node_name = module.params['record_fqdn'].replace('.' + module.params['zone'], '')

    # Retrieve the zone object from dyn
    dyn_zone = Zone(module.params['zone'])

    # Retrieve the node object from dyn
    dyn_node = dyn_zone.get_node(node=dyn_node_name)

    # All states will need a list of the exiting records for the zone.
    dyn_node_records = get_any_records(module, dyn_node)

    if module.params['state'] == 'list':
        module.exit_json(changed=False,
                         records=get_record_values(
                             dyn_node_records,
                         ))

    if module.params['state'] == 'present':

        # First get a list of existing records for the node
        values = get_record_values(dyn_node_records)
        value_key = get_record_key(module.params['record_type'])
        param_value = module.params['record_value']

        # Check to see if the record is already in place before doing anything.
        if (dyn_node_records and
                dyn_node_records[value_key][0].ttl == module.params['record_ttl'] and
                (param_value in values[value_key] or
                 param_value + '.' in values[value_key])):

            module.exit_json(changed=False)


        # Working on the assumption that there is only one record per
        # node we will first delete the node if there are any records before
        # creating the correct record
        if dyn_node_records:
            dyn_node.delete()

        # Now lets create the correct node entry.
        dyn_zone.add_record(dyn_node_name,
                            module.params['record_type'],
                            module.params['record_value'],
                            module.params['record_ttl']
                           )

        # Now publish the zone since we've updated it.
        dyn_zone.publish()
        module.exit_json(changed=True,
                         msg="Created node %s in zone %s" % (dyn_node_name, module.params['zone']))

    if module.params['state'] == 'absent':
        # If there are any records present we'll want to delete the node.
        if dyn_node_records:
            dyn_node.delete()
            # Publish the zone since we've modified it.
            dyn_zone.publish()
            module.exit_json(changed=True,
                             msg="Removed node %s from zone %s" % (dyn_node_name, module.params['zone']))
        else:
            module.exit_json(changed=False)

# Ansible tends to need a wild card import so we'll use it here
# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
from ansible.module_utils.basic import *
if __name__ == '__main__':
    main()