#!/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()
