diff --git a/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_dhcp_options.py b/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_dhcp_options.py index 106f91259c..99b6fbbd6a 100644 --- a/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_dhcp_options.py +++ b/lib/ansible/modules/extras/cloud/amazon/ec2_vpc_dhcp_options.py @@ -16,13 +16,21 @@ DOCUMENTATION = """ --- module: ec2_vpc_dhcp_options -short_description: Ensures the DHCP options for the given VPC match what's +short_description: Manages DHCP Options, and can ensure the DHCP options for the given VPC match what's requested description: - - Converges the DHCP option set for the given VPC to the variables requested. + - 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) or a purge of - existing options. Most of the options should be self-explanatory. + 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: @@ -30,37 +38,40 @@ options: description: - The domain name to set in the DHCP option sets required: false - default: "" + 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: [] + default: None ntp_servers: description: - List of hosts to advertise as NTP servers for the VPC. required: false - default: [] + default: None netbios_name_servers: description: - List of hosts to advertise as NetBIOS servers. required: false - default: [] + default: None netbios_node_type: description: - - NetBIOS node type to advertise in the DHCP options. The - default is 2, per AWS recommendation + - 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: 2 + default: None vpc_id: description: - - VPC ID to associate with the requested DHCP option set - required: true + - 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 creating a new one. + - 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 @@ -74,6 +85,28 @@ options: 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'] + 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 + 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' ] extends_documentation_fragment: aws requirements: - boto @@ -81,19 +114,26 @@ requirements: RETURN = """ new_options: - description: The new DHCP options associated with your VPC - returned: changed - 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 - ntp-servers: None - netbios-node-type: 2 - domain-name: "my.example.com" + 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 = """ @@ -127,105 +167,212 @@ EXAMPLES = """ 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_options: + 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_options: + 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_options: + region: us-east-1 + dhcp_options_id: dopt-12345678 + vpc_id: vpc-123456 + """ import boto.vpc +import boto.ec2 +from boto.exception import EC2ResponseError import socket import collections -def _get_associated_dhcp_options(vpc_id, vpc_connection): +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(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=get_error_message(e.args[2])) + +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_connection.get_all_vpcs(vpc_ids=[vpc_id]) - if len(vpcs) != 1: - return None - dhcp_options = vpc_connection.get_all_dhcp_options(dhcp_options_ids=[vpcs[0].dhcp_options_id]) + 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 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 _get_vpcs_by_dhcp_options(dhcp_options_id, vpc_connection): - return vpc_connection.get_all_vpcs(filters={'dhcpOptionsId': dhcp_options_id}) - - -def _get_updated_option(requested, existing, inherit): - if inherit and (not requested or requested == ['']): - return existing +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: - return requested - + vpc_conn.delete_dhcp_options(dhcp_options_id) + return True def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - domain_name=dict(type='str', default=''), - dns_servers=dict(type='list', default=[]), - ntp_servers=dict(type='list', default=[]), - netbios_name_servers=dict(type='list', default=[]), - netbios_node_type=dict(type='int', default=2), - vpc_id=dict(type='str', required=True), + 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) + 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) region, ec2_url, boto_params = get_aws_connection_info(module) connection = connect_to_aws(boto.vpc, region, **boto_params) - - inherit_existing = params['inherit_existing'] - - existing_options = _get_associated_dhcp_options(params['vpc_id'], connection) - new_options = collections.defaultdict(lambda: None) - - new_options['domain-name-servers'] = _get_updated_option( params['dns_servers'], - existing_options.options.get('domain-name-servers'), inherit_existing) - - new_options['netbios-name-servers'] = _get_updated_option(params['netbios_name_servers'], - existing_options.options.get('netbios-name-servers'), inherit_existing) - - - new_options['ntp-servers'] = _get_updated_option(params['ntp_servers'], - existing_options.options.get('ntp-servers'), inherit_existing) - - # HACK: Why do I make the next two lists? The boto api returns a list if present, so - # I need this to properly compare so == works. - - # HACK: netbios-node-type is an int, but boto returns a string. So, asking for an int from Ansible - # for data validation, but still need to cast it to a string - new_options['netbios-node-type'] = _get_updated_option( - [str(params['netbios_node_type'])], existing_options.options.get('netbios-node-type'), - inherit_existing) - - new_options['domain-name'] = _get_updated_option( - [params['domain_name']], existing_options.options.get('domain-name'), inherit_existing) - - if existing_options and new_options == existing_options.options: - module.exit_json(changed=False) - - 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] - - if not module.check_mode: - 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']) + + 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'] != None: + new_options['domain-name-servers'] = params['dns_servers'] + if params['netbios_name_servers'] != None: + new_options['netbios-name-servers'] = params['netbios_name_servers'] + if params['ntp_servers'] != None: + new_options['ntp-servers'] = params['ntp_servers'] + if params['domain_name'] != None: + # needs to be a list for comparison with boto objects later + new_options['domain-name'] = [ params['domain_name'] ] + if params['netbios_node_type'] != 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(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(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: - other_vpcs = _get_vpcs_by_dhcp_options(existing_options.id, connection) - if len(other_vpcs) == 0 or (len(other_vpcs) == 1 and other_vpcs[0].id == params['vpc_id']): - connection.delete_dhcp_options(existing_options.id) + remove_dhcp_options_by_id(connection, existing_options.id) - module.exit_json(changed=True, new_options=new_options) + module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=dhcp_option.id) from ansible.module_utils.basic import *