summaryrefslogtreecommitdiff
path: root/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option.py
blob: 2ca3fff34a45439a912a238915094b4ed97b41c6 (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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/usr/bin/python
# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['stableinterface'],
                    'supported_by': 'curated'}


DOCUMENTATION = """
---
module: ec2_vpc_dhcp_option
short_description: Manages DHCP Options, and can ensure the DHCP options for the given VPC match what's
  requested
description:
  - This module removes, or creates DHCP option sets, and can associate them to a VPC.
    Optionally, a new DHCP Options set can be created that converges a VPC's existing
    DHCP option set with values provided.
    When dhcp_options_id is provided, the module will
    1. remove (with state='absent')
    2. ensure tags are applied (if state='present' and tags are provided
    3. attach it to a VPC (if state='present' and a vpc_id is provided.
    If any of the optional values are missing, they will either be treated
    as a no-op (i.e., inherit what already exists for the VPC)
    To remove existing options while inheriting, supply an empty value
    (e.g. set ntp_servers to [] if you want to remove them from the VPC's options)
    Most of the options should be self-explanatory.
author: "Joel Thompson (@joelthompson)"
version_added: 2.1
options:
  domain_name:
    description:
      - The domain name to set in the DHCP option sets
    required: false
    default: None
  dns_servers:
    description:
      - A list of hosts to set the DNS servers for the VPC to. (Should be a
        list of IP addresses rather than host names.)
    required: false
    default: None
  ntp_servers:
    description:
      - List of hosts to advertise as NTP servers for the VPC.
    required: false
    default: None
  netbios_name_servers:
    description:
      - List of hosts to advertise as NetBIOS servers.
    required: false
    default: None
  netbios_node_type:
    description:
      - NetBIOS node type to advertise in the DHCP options.
        The AWS recommendation is to use 2 (when using netbios name services)
        http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html
    required: false
    default: None
  vpc_id:
    description:
      - VPC ID to associate with the requested DHCP option set.
        If no vpc id is provided, and no matching option set is found then a new
        DHCP option set is created.
    required: false
    default: None
  delete_old:
    description:
      - Whether to delete the old VPC DHCP option set when associating a new one.
        This is primarily useful for debugging/development purposes when you
        want to quickly roll back to the old option set. Note that this setting
        will be ignored, and the old DHCP option set will be preserved, if it
        is in use by any other VPC. (Otherwise, AWS will return an error.)
    required: false
    default: true
  inherit_existing:
    description:
      - For any DHCP options not specified in these parameters, whether to
        inherit them from the options set already applied to vpc_id, or to
        reset them to be empty.
    required: false
    default: false
  tags:
    description:
      - Tags to be applied to a VPC options set if a new one is created, or
        if the resource_id is provided. (options must match)
    required: False
    default: None
    aliases: [ 'resource_tags']
    version_added: "2.1"
  dhcp_options_id:
    description:
      - The resource_id of an existing DHCP options set.
        If this is specified, then it will override other settings, except tags
        (which will be updated to match)
    required: False
    default: None
    version_added: "2.1"
  state:
    description:
      - create/assign or remove the DHCP options.
        If state is set to absent, then a DHCP options set matched either
        by id, or tags and options will be removed if possible.
    required: False
    default: present
    choices: [ 'absent', 'present' ]
    version_added: "2.1"
extends_documentation_fragment: aws
requirements:
    - boto
"""

RETURN = """
new_options:
    description: The DHCP options created, associated or found
    returned: when appropriate
    type: dict
    sample:
      domain-name-servers:
        - 10.0.0.1
        - 10.0.1.1
      netbois-name-servers:
        - 10.0.0.1
        - 10.0.1.1
      netbios-node-type: 2
      domain-name: "my.example.com"
dhcp_options_id:
    description: The aws resource id of the primary DCHP options set created, found or removed
    type: string
    returned: when available
changed:
    description: Whether the dhcp options were changed
    type: bool
    returned: always
"""

EXAMPLES = """
# Completely overrides the VPC DHCP options associated with VPC vpc-123456 and deletes any existing
# DHCP option set that may have been attached to that VPC.
- ec2_vpc_dhcp_option:
    domain_name: "foo.example.com"
    region: us-east-1
    dns_servers:
        - 10.0.0.1
        - 10.0.1.1
    ntp_servers:
        - 10.0.0.2
        - 10.0.1.2
    netbios_name_servers:
        - 10.0.0.1
        - 10.0.1.1
    netbios_node_type: 2
    vpc_id: vpc-123456
    delete_old: True
    inherit_existing: False


# Ensure the DHCP option set for the VPC has 10.0.0.4 and 10.0.1.4 as the specified DNS servers, but
# keep any other existing settings. Also, keep the old DHCP option set around.
- ec2_vpc_dhcp_option:
    region: us-east-1
    dns_servers:
      - "{{groups['dns-primary']}}"
      - "{{groups['dns-secondary']}}"
    vpc_id: vpc-123456
    inherit_existing: True
    delete_old: False


## Create a DHCP option set with 4.4.4.4 and 8.8.8.8 as the specified DNS servers, with tags
## but do not assign to a VPC
- ec2_vpc_dhcp_option:
    region: us-east-1
    dns_servers:
      - 4.4.4.4
      - 8.8.8.8
    tags:
      Name: google servers
      Environment: Test

## Delete a DHCP options set that matches the tags and options specified
- ec2_vpc_dhcp_option:
    region: us-east-1
    dns_servers:
      - 4.4.4.4
      - 8.8.8.8
    tags:
      Name: google servers
      Environment: Test
  state: absent

## Associate a DHCP options set with a VPC by ID
- ec2_vpc_dhcp_option:
    region: us-east-1
    dhcp_options_id: dopt-12345678
    vpc_id: vpc-123456

"""

import collections
import traceback

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import HAS_BOTO, connect_to_aws, ec2_argument_spec, get_aws_connection_info

if HAS_BOTO:
    import boto.vpc
    import boto.ec2
    from boto.exception import EC2ResponseError


def get_resource_tags(vpc_conn, resource_id):
    return dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': resource_id}))


def ensure_tags(module, vpc_conn, resource_id, tags, add_only, check_mode):
    try:
        cur_tags = get_resource_tags(vpc_conn, resource_id)
        if tags == cur_tags:
            return {'changed': False, 'tags': cur_tags}

        to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags)
        if to_delete and not add_only:
            vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode)

        to_add = dict((k, tags[k]) for k in tags if k not in cur_tags)
        if to_add:
            vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode)

        latest_tags = get_resource_tags(vpc_conn, resource_id)
        return {'changed': True, 'tags': latest_tags}
    except EC2ResponseError as e:
        module.fail_json(msg="Failed to modify tags: %s" % e.message, exception=traceback.format_exc())

def fetch_dhcp_options_for_vpc(vpc_conn, vpc_id):
    """
    Returns the DHCP options object currently associated with the requested VPC ID using the VPC
    connection variable.
    """
    vpcs = vpc_conn.get_all_vpcs(vpc_ids=[vpc_id])
    if len(vpcs) != 1 or vpcs[0].dhcp_options_id == "default":
        return None
    dhcp_options = vpc_conn.get_all_dhcp_options(dhcp_options_ids=[vpcs[0].dhcp_options_id])
    if len(dhcp_options) != 1:
        return None
    return dhcp_options[0]


def match_dhcp_options(vpc_conn, tags=None, options=None):
    """
    Finds a DHCP Options object that optionally matches the tags and options provided
    """
    dhcp_options = vpc_conn.get_all_dhcp_options()
    for dopts in dhcp_options:
        if (not tags) or get_resource_tags(vpc_conn, dopts.id) == tags:
            if (not options) or dopts.options == options:
                return(True, dopts)
    return(False, None)


def remove_dhcp_options_by_id(vpc_conn, dhcp_options_id):
    associations = vpc_conn.get_all_vpcs(filters={'dhcpOptionsId': dhcp_options_id})
    if len(associations) > 0:
        return False
    else:
        vpc_conn.delete_dhcp_options(dhcp_options_id)
        return True


def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(dict(
        dhcp_options_id=dict(type='str', default=None),
        domain_name=dict(type='str', default=None),
        dns_servers=dict(type='list', default=None),
        ntp_servers=dict(type='list', default=None),
        netbios_name_servers=dict(type='list', default=None),
        netbios_node_type=dict(type='int', default=None),
        vpc_id=dict(type='str', default=None),
        delete_old=dict(type='bool', default=True),
        inherit_existing=dict(type='bool', default=False),
        tags=dict(type='dict', default=None, aliases=['resource_tags']),
        state=dict(type='str', default='present', choices=['present', 'absent'])
        )
    )

    module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
    params = module.params
    found = False
    changed = False
    new_options = collections.defaultdict(lambda: None)

    if not HAS_BOTO:
        module.fail_json(msg='boto is required for this module')

    region, ec2_url, boto_params = get_aws_connection_info(module)
    connection = connect_to_aws(boto.vpc, region, **boto_params)

    existing_options = None

    # First check if we were given a dhcp_options_id
    if not params['dhcp_options_id']:
        # No, so create new_options from the parameters
        if params['dns_servers'] is not None:
            new_options['domain-name-servers'] = params['dns_servers']
        if params['netbios_name_servers'] is not None:
            new_options['netbios-name-servers'] = params['netbios_name_servers']
        if params['ntp_servers'] is not None:
            new_options['ntp-servers'] = params['ntp_servers']
        if params['domain_name'] is not None:
            # needs to be a list for comparison with boto objects later
            new_options['domain-name'] = [ params['domain_name'] ]
        if params['netbios_node_type'] is not None:
            # needs to be a list for comparison with boto objects later
            new_options['netbios-node-type'] = [ str(params['netbios_node_type']) ]
        # If we were given a vpc_id then we need to look at the options on that
        if params['vpc_id']:
            existing_options = fetch_dhcp_options_for_vpc(connection, params['vpc_id'])
            # if we've been asked to inherit existing options, do that now
            if params['inherit_existing']:
                if existing_options:
                    for option in [ 'domain-name-servers', 'netbios-name-servers', 'ntp-servers', 'domain-name', 'netbios-node-type']:
                        if existing_options.options.get(option) and new_options[option] != [] and (not new_options[option] or [''] == new_options[option]):
                            new_options[option] = existing_options.options.get(option)

            # Do the vpc's dhcp options already match what we're asked for? if so we are done
            if existing_options and new_options == existing_options.options:
                module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=existing_options.id)

        # If no vpc_id was given, or the options don't match then look for an existing set using tags
        found, dhcp_option = match_dhcp_options(connection, params['tags'], new_options)

    # Now let's cover the case where there are existing options that we were told about by id
    # If a dhcp_options_id was supplied we don't look at options inside, just set tags (if given)
    else:
        supplied_options = connection.get_all_dhcp_options(filters={'dhcp-options-id':params['dhcp_options_id']})
        if len(supplied_options) != 1:
            if params['state'] != 'absent':
                module.fail_json(msg=" a dhcp_options_id was supplied, but does not exist")
        else:
            found = True
            dhcp_option = supplied_options[0]
            if params['state'] != 'absent' and params['tags']:
                ensure_tags(module, connection, dhcp_option.id, params['tags'], False, module.check_mode)

    # Now we have the dhcp options set, let's do the necessary

    # if we found options we were asked to remove then try to do so
    if params['state'] == 'absent':
        if not module.check_mode:
            if found:
                changed = remove_dhcp_options_by_id(connection, dhcp_option.id)
        module.exit_json(changed=changed, new_options={})

    # otherwise if we haven't found the required options we have something to do
    elif not module.check_mode and not found:

        # create some dhcp options if we weren't able to use existing ones
        if not found:
            # Convert netbios-node-type and domain-name back to strings
            if new_options['netbios-node-type']:
                new_options['netbios-node-type'] = new_options['netbios-node-type'][0]
            if new_options['domain-name']:
                new_options['domain-name'] = new_options['domain-name'][0]

            # create the new dhcp options set requested
            dhcp_option = connection.create_dhcp_options(
                new_options['domain-name'],
                new_options['domain-name-servers'],
                new_options['ntp-servers'],
                new_options['netbios-name-servers'],
                new_options['netbios-node-type'])
            changed = True
            if params['tags']:
                ensure_tags(module, connection, dhcp_option.id, params['tags'], False, module.check_mode)

    # If we were given a vpc_id, then attach the options we now have to that before we finish
    if params['vpc_id'] and not module.check_mode:
        changed = True
        connection.associate_dhcp_options(dhcp_option.id, params['vpc_id'])
        # and remove old ones if that was requested
        if params['delete_old'] and existing_options:
            remove_dhcp_options_by_id(connection, existing_options.id)

    module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=dhcp_option.id)


if __name__ == "__main__":
    main()