From 00af464b8a069e23840f545a8a644aa3dc488b62 Mon Sep 17 00:00:00 2001 From: Brice Burgess Date: Thu, 25 Jul 2013 15:14:05 -0500 Subject: [PATCH] initial import of dnsmadeeasy module --- net_infrastructure/dnsmadeeasy | 345 +++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 net_infrastructure/dnsmadeeasy diff --git a/net_infrastructure/dnsmadeeasy b/net_infrastructure/dnsmadeeasy new file mode 100644 index 00000000000..542b6f8e285 --- /dev/null +++ b/net_infrastructure/dnsmadeeasy @@ -0,0 +1,345 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: dnsmadeeasy +version_added: "1.3" +short_description: Interface with dnsmadeeasy.com (a DNS hosting service). +description: + - Manage DNS records via the v2 REST API of the DNS Made Easy service. Records only; no manipulation of domains or monitor/account support yet. See: http://www.dnsmadeeasy.com/services/rest-api/ +options: + account_key: + description: + - Accout API Key. + required: true + default: null + + account_secret: + description: + - Accout Secret Key. + required: true + default: null + + domain: + description: + - Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNS Made Easy (e.g. "839989") for faster resolution. + required: true + default: null + + record_name: + description: + - Record name (to get/create/delete/update). + If record_name is not specified; all records for the domain will be returned as "result" regardless of state argument. + required: false + default: null + + record_type: + description: + - Record type. + required: false + choices: [ 'A', 'AAAA', 'CNAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT' ] + default: null + + record_value: + description: + - Record value. HTTPRED: , MX: , NS: , PTR: , SRV: , TXT: + If record_value is not specified; no changes will be made and the record will be returned as "result" (e.g. can be used to fetch a record's current id, type, and ttl) + required: false + default: null + + record_ttl: + description: + - Record "Time to live". Number of seconds a record remains cached in DNS servers. + required: false + default: 1800 + + state: + description: + - If state is "present", record will be created. If state is "present" and if record exists and values have changed, it will be updated. + If state is absent, record will be removed. + required: true + choices: [ 'present', 'absent' ] + default: null + +notes: + - The DNS Made Easy service requires that machines interacting with it's API have the proper time + timezone set. Be sure you're within a few seconds of actual GMT by using NTP. + - This module returns record(s) as "result" when state == 'present'. It can be be registered and used in your playbooks. + +requirements: [ urllib, urllib2, hashlib, hmac ] +author: Brice Burgess +''' + +EXAMPLES = ''' +# fetch my.com domain records +- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present + register: response + +# create / ensure the presence of a record +- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_type="A" record_value="127.0.0.1" + +# update the previously created record +- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_value="192.168.0.1" + +# fetch a specific record +- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" + register: response + +# delete a record / ensure it is absent +- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=absent record_name="test" +''' + +# ============================================ +# DNSMadeEasy module specific support methods. +# + +IMPORT_ERROR = None +try: + import urllib + import urllib2 + import json + from time import strftime, gmtime + import hashlib + import hmac +except ImportError, e: + IMPORT_ERROR = str(e) + + +class RequestWithMethod(urllib2.Request): + + """Workaround for using DELETE/PUT/etc with urllib2""" + + def __init__(self, url, method, data=None, headers={}): + self._method = method + urllib2.Request.__init__(self, url, data, headers) + + def get_method(self): + if self._method: + return self._method + else: + return urllib2.Request.get_method(self) + + +class DME2: + + def __init__(self, apikey, secret, domain, module): + self.module = module + + self.api = apikey + self.secret = secret + self.baseurl = 'http://api.dnsmadeeasy.com/V2.0/' + self.domain = str(domain) + self.domain_map = None # ["domain_name"] => ID + self.record_map = None # ["record_name"] => ID + self.records = None # ["record_ID"] => + + # Lookup the domain ID if passed as a domain name vs. ID + if not self.domain.isdigit(): + self.domain = self.getDomainByName(self.domain)['id'] + + self.record_url = 'dns/managed/' + str(self.domain) + '/records' + + def _headers(self): + currTime = self._get_date() + hashstring = self._create_hash(currTime) + headers = {'x-dnsme-apiKey': self.api, + 'x-dnsme-hmac': hashstring, + 'x-dnsme-requestDate': currTime, + 'content-type': 'application/json'} + return headers + + def _get_date(self): + return strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime()) + + def _create_hash(self, rightnow): + return hmac.new(self.secret.encode(), rightnow.encode(), hashlib.sha1).hexdigest() + + def query(self, resource, method, data=None): + url = self.baseurl + resource + if data and not isinstance(data, basestring): + data = urllib.urlencode(data) + request = RequestWithMethod(url, method, data, self._headers()) + + try: + response = urllib2.urlopen(request) + except urllib2.HTTPError, e: + self.module.fail_json( + msg="%s returned %s, with body: %s" % (url, e.code, e.read())) + except Exception, e: + self.module.fail_json( + msg="Failed contacting: %s : Exception %s" % (url, e.message())) + + try: + return json.load(response) + except Exception, e: + return False + + def getDomain(self, domain_id): + if not self.domain_map: + self._instMap('domain') + + return self.domains.get(domain_id, False) + + def getDomainByName(self, domain_name): + if not self.domain_map: + self._instMap('domain') + + return self.getDomain(self.domain_map.get(domain_name, 0)) + + def getDomains(self): + return self.query('dns/managed', 'GET')['data'] + + def getRecord(self, record_id): + if not self.record_map: + self._instMap('record') + + return self.records.get(record_id, False) + + def getRecordByName(self, record_name): + if not self.record_map: + self._instMap('record') + + return self.getRecord(self.record_map.get(record_name, 0)) + + def getRecords(self): + return self.query(self.record_url, 'GET')['data'] + + def _instMap(self, type): + #@TODO cache this call so it's executed only once per ansible execution + map = {} + results = {} + + # iterate over e.g. self.getDomains() || self.getRecords() + for result in getattr(self, 'get' + type.title() + 's')(): + + map[result['name']] = result['id'] + results[result['id']] = result + + # e.g. self.domain_map || self.record_map + setattr(self, type + '_map', map) + setattr(self, type + 's', results) # e.g. self.domains || self.records + + def prepareRecord(self, data): + return json.dumps(data, separators=(',', ':')) + + def createRecord(self, data): + #@TODO update the cache w/ resultant record + id when impleneted + return self.query(self.record_url, 'POST', data) + + def updateRecord(self, record_id, data): + #@TODO update the cache w/ resultant record + id when impleneted + return self.query(self.record_url + '/' + str(record_id), 'PUT', data) + + def deleteRecord(self, record_id): + #@TODO remove record from the cache when impleneted + return self.query(self.record_url + '/' + str(record_id), 'DELETE') + + +# =========================================== +# Module execution. +# + +def main(): + + module = AnsibleModule( + argument_spec=dict( + account_key=dict(required=True), + account_secret=dict(required=True, no_log=True), + domain=dict(required=True), + state=dict(required=True, choices=['present', 'absent']), + record_name=dict(required=False), + record_type=dict(required=False, choices=[ + 'A', 'AAAA', 'CNAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT']), + record_value=dict(required=False), + record_ttl=dict(required=False, default=1800, type='int'), + #redirecttype=dict(required=False,default='Hidden Frame Masked',choices=[ 'Hidden Frame Masked', 'Standard 301', 'Standard 302' ]), + #hardlink=dict(requred=False, default=False, type='bool', choices=BOOLEANS) + ), + required_together=( + ['record_value', 'record_ttl', 'record_type'] + ) + ) + + if IMPORT_ERROR: + module.fail_json(msg="Import Error: " + IMPORT_ERROR) + + DME = DME2(module.params["account_key"], module.params[ + "account_secret"], module.params["domain"], module) + state = module.params["state"] + record_name = module.params["record_name"] + + # Follow Keyword Controlled Behavior + if not record_name: + domain_records = DME.getRecords() + if not domain_records: + module.fail_json( + msg="The %s domain name is not accessible with this api_key; try using its ID if known." % domain) + module.exit_json(changed=False, result=domain_records) + + # Fetch existing record + Build new one + current_record = DME.getRecordByName(record_name) + new_record = {'name': record_name} + for i in ["record_value", "record_type", "record_ttl"]: + if module.params[i]: + new_record[i[len("record_"):]] = module.params[i] + + # Compare new record against existing one + changed = False + if current_record: + for i in new_record: + if str(current_record[i]) != str(new_record[i]): + changed = True + new_record['id'] = str(current_record['id']) + + # Follow Keyword Controlled Behavior + if state == 'present': + # return the record if no value is specified + if not "value" in new_record: + if not current_record: + module.fail_json( + msg="A record with name '%s' does not exist for domain '%s.'" % (record_name, domain)) + module.exit_json(changed=False, result=current_record) + + # create record as it does not exist + if not current_record: + record = DME.createRecord(DME.prepareRecord(new_record)) + module.exit_json(changed=True, result=record) + + # update the record + if changed: + DME.updateRecord( + current_record['id'], DME.prepareRecord(new_record)) + module.exit_json(changed=True, result=new_record) + + # return the record (no changes) + module.exit_json(changed=False, result=current_record) + + elif state == 'absent': + # delete the record if it exists + if current_record: + DME.deleteRecord(current_record['id']) + module.exit_json(changed=True) + + # record does not exist, return w/o change. + module.exit_json(changed=False) + + else: + module.fail_json( + msg="'%s' is an unknown value for the state argument" % state) + +# this is magic, see lib/ansible/module_common.py +#<> +main()