Merge pull request #3670 from briceburg/devel

NEW MODULE: dnsmadeeasy
This commit is contained in:
Michael DeHaan 2013-08-03 11:31:07 -07:00
commit fa647e43ee

View file

@ -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 <http://www.gnu.org/licenses/>.
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: <redirection URL>, MX: <priority> <target name>, NS: <name server>, PTR: <target name>, SRV: <priority> <weight> <port> <target name>, TXT: <text value>
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"] => <record>
# 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
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()