From fd1978777d153d27f2c062582acf31a293958503 Mon Sep 17 00:00:00 2001 From: Russell Harrison Date: Thu, 28 Jan 2016 14:55:29 -0500 Subject: WIP adding the lib_dyn role for the dyn_record module --- roles/lib_dyn/README.md | 38 +++++ roles/lib_dyn/defaults/main.yml | 2 + roles/lib_dyn/handlers/main.yml | 2 + roles/lib_dyn/library/dyn_record.py | 272 ++++++++++++++++++++++++++++++++++++ roles/lib_dyn/meta/main.yml | 147 +++++++++++++++++++ roles/lib_dyn/tasks/main.yml | 2 + roles/lib_dyn/vars/main.yml | 2 + 7 files changed, 465 insertions(+) create mode 100644 roles/lib_dyn/README.md create mode 100644 roles/lib_dyn/defaults/main.yml create mode 100644 roles/lib_dyn/handlers/main.yml create mode 100644 roles/lib_dyn/library/dyn_record.py create mode 100644 roles/lib_dyn/meta/main.yml create mode 100644 roles/lib_dyn/tasks/main.yml create mode 100644 roles/lib_dyn/vars/main.yml diff --git a/roles/lib_dyn/README.md b/roles/lib_dyn/README.md new file mode 100644 index 000000000..225dd44b9 --- /dev/null +++ b/roles/lib_dyn/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/lib_dyn/defaults/main.yml b/roles/lib_dyn/defaults/main.yml new file mode 100644 index 000000000..ccc2e8561 --- /dev/null +++ b/roles/lib_dyn/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for lib_dyn diff --git a/roles/lib_dyn/handlers/main.yml b/roles/lib_dyn/handlers/main.yml new file mode 100644 index 000000000..2074235d2 --- /dev/null +++ b/roles/lib_dyn/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for lib_dyn diff --git a/roles/lib_dyn/library/dyn_record.py b/roles/lib_dyn/library/dyn_record.py new file mode 100644 index 000000000..0ddacbe8b --- /dev/null +++ b/roles/lib_dyn/library/dyn_record.py @@ -0,0 +1,272 @@ +#!/usr/bin/python +# +# (c) 2015, Russell Harrison +# +# 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" +''' + +try: + from dyn.tm.session import DynectSession + from dyn.tm.zones import get_all_zones + from dyn.tm.zones import Zone + import dyn.tm.errors + import json + import os + import sys + IMPORT_ERROR = False +except ImportError as e: + IMPORT_ERROR = str(e) + +# 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 add_record(module, zone, type, value_key, value, ): + # Add a new record to the zone. + try: + getattr(self, 'add_record') + except: + module.fail_json(msg='Unable to add record: ') + +def get_record_type(record_key): + return record_key.replace('_records', '').upper() + +def get_record_key(record_type): + return record_type.lower() + '_records' + +def get_any_records(module, node): + # Lets get a list of the A records for the node + try: + records = node.get_any_records() + except dyn.tm.errors.DynectGetError as e: + if 'Not in zone' in str(e): + # 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(e)) + + # Return a dictionary of the record objects + return records + +def get_record_values(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(): + module = AnsibleModule( + argument_spec=dict( + state=dict(required=True, 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), + record_fqdn=dict(required=False), + record_type=dict(required=False, choices=[ + 'A', 'AAAA', 'CNAME', 'PTR', 'TXT']), + record_value=dict(required=False), + 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: + dyn_session = DynectSession(module.params['customer_name'], + module.params['user_name'], + module.params['user_password']) + except dyn.tm.errors.DynectAuthError as e: + module.fail_json(msg='Unable to authenticate with Dyn', + error=str(e)) + + # Retrieve zone object + try: + dyn_zone = Zone(module.params['zone']) + except dyn.tm.errors.DynectGetError as e: + if 'No such zone' in e: + 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(e)) + + + # 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']) + + # 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 + module.params['record_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) + + + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/roles/lib_dyn/meta/main.yml b/roles/lib_dyn/meta/main.yml new file mode 100644 index 000000000..3d725d002 --- /dev/null +++ b/roles/lib_dyn/meta/main.yml @@ -0,0 +1,147 @@ +--- +galaxy_info: + author: Russell Harrison + description: A role to provide the dyn_record module + company: Red Hat, Inc. + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + # Some suggested licenses: + # - BSD (default) + # - MIT + # - GPLv2 + # - GPLv3 + # - Apache + # - CC-BY + license: Apache + min_ansible_version: 1.9 + # + # Below are all platforms currently available. Just uncomment + # the ones that apply to your role. If you don't see your + # platform on this list, let us know and we'll get it added! + # + platforms: + - name: EL + versions: + # - all + # - 5 + # - 6 + - 7 + #- name: GenericUNIX + # versions: + # - all + # - any + #- name: Solaris + # versions: + # - all + # - 10 + # - 11.0 + # - 11.1 + # - 11.2 + # - 11.3 + #- name: Fedora + # versions: + # - all + # - 16 + # - 17 + # - 18 + # - 19 + # - 20 + # - 21 + # - 22 + #- name: Windows + # versions: + # - all + # - 2012R2 + #- name: SmartOS + # versions: + # - all + # - any + #- name: opensuse + # versions: + # - all + # - 12.1 + # - 12.2 + # - 12.3 + # - 13.1 + # - 13.2 + #- name: Amazon + # versions: + # - all + # - 2013.03 + # - 2013.09 + #- name: GenericBSD + # versions: + # - all + # - any + #- name: FreeBSD + # versions: + # - all + # - 8.0 + # - 8.1 + # - 8.2 + # - 8.3 + # - 8.4 + # - 9.0 + # - 9.1 + # - 9.1 + # - 9.2 + #- name: Ubuntu + # versions: + # - all + # - lucid + # - maverick + # - natty + # - oneiric + # - precise + # - quantal + # - raring + # - saucy + # - trusty + # - utopic + # - vivid + #- name: SLES + # versions: + # - all + # - 10SP3 + # - 10SP4 + # - 11 + # - 11SP1 + # - 11SP2 + # - 11SP3 + #- name: GenericLinux + # versions: + # - all + # - any + #- name: Debian + # versions: + # - all + # - etch + # - jessie + # - lenny + # - squeeze + # - wheezy + # + # Below are all categories currently available. Just as with + # the platforms above, uncomment those that apply to your role. + # + categories: + #- cloud + #- cloud:ec2 + #- cloud:gce + #- cloud:rax + #- clustering + #- database + #- database:nosql + #- database:sql + #- development + #- monitoring + - networking + #- packaging + #- system + #- web +dependencies: [] + # List your role dependencies here, one per line. + # Be sure to remove the '[]' above if you add dependencies + # to this list. + diff --git a/roles/lib_dyn/tasks/main.yml b/roles/lib_dyn/tasks/main.yml new file mode 100644 index 000000000..35ee62e81 --- /dev/null +++ b/roles/lib_dyn/tasks/main.yml @@ -0,0 +1,2 @@ +--- +# tasks file for lib_dyn diff --git a/roles/lib_dyn/vars/main.yml b/roles/lib_dyn/vars/main.yml new file mode 100644 index 000000000..4b2f35586 --- /dev/null +++ b/roles/lib_dyn/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for lib_dyn -- cgit v1.2.3