#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c)2012 Rackspace US, Inc.

# All Rights Reserved.
#
#    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.

from functools import wraps
import json
import re
import time

import six

import pyos
from pyos.client import BaseClient
from pyos.cloudloadbalancers import CloudLoadBalancer
import pyos.exceptions as exc
from pyos.manager import BaseManager
from pyos.resource import BaseResource
import pyos.utils as utils

# How long (in seconds) to wait for a response from async operations
DEFAULT_TIMEOUT = 5
# How long (in seconds) to wait in between checks for async completion
DEFAULT_DELAY = 0.5
# How many times to retry a GET before raising an error
DEFAULT_RETRY = 3


def assure_domain(fnc):
    @wraps(fnc)
    def _wrapped(self, domain, *args, **kwargs):
        if not isinstance(domain, CloudDNSDomain):
            # Must be the ID or name. Try ID first:
            try:
                domain = self._manager.get(domain)
            except exc.NotFound:
                domain = self._manager.find(name=domain)
        return fnc(self, domain, *args, **kwargs)
    return _wrapped



class CloudDNSRecord(BaseResource):
    """
    This class represents a domain record.
    """
    GET_DETAILS = False
    # Initialize the supported attributes.
    type = None
    name = None
    data = None
    priority = None
    ttl = None
    comment = None


    def update(self, data=None, priority=None, ttl=None, comment=None):
        """
        Modifies this record.
        """
        return self.manager.update_record(self.domain_id, self, data=data,
                priority=priority, ttl=ttl, comment=comment)


    def get(self):
        """
        Gets the full information for an existing record for this domain.
        """
        return self.manager.get_record(self.domain_id, self)


    def delete(self):
        """
        Deletes an existing record for this domain.
        """
        return self.manager.delete_record(self.domain_id, self)



class CloudDNSDomain(BaseResource):
    """
    This class represents a DNS domain.
    """
    def delete(self, delete_subdomains=False):
        """
        Deletes this domain and all of its resource records. If this domain has
        subdomains, each subdomain will now become a root domain. If you wish to
        also delete any subdomains, pass True to 'delete_subdomains'.
        """
        self.manager.delete(self, delete_subdomains=delete_subdomains)


    def changes_since(self, date_or_datetime):
        """
        Gets the changes for this domain since the specified date/datetime.
        The date can be one of:
            - a Python datetime object
            - a Python date object
            - a string in the format 'YYYY-MM-YY HH:MM:SS'
            - a string in the format 'YYYY-MM-YY'

        It returns a list of dicts, whose keys depend on the specific change
        that was made. A simple example of such a change dict:

            {u'accountId': 000000,
             u'action': u'update',
             u'changeDetails': [{u'field': u'serial_number',
               u'newValue': u'1354038941',
               u'originalValue': u'1354038940'},
              {u'field': u'updated_at',
               u'newValue': u'Tue Nov 27 17:55:41 UTC 2012',
               u'originalValue': u'Tue Nov 27 17:55:40 UTC 2012'}],
             u'domain': u'example.com',
             u'targetId': 00000000,
             u'targetType': u'Domain'}
        """
        return self.manager.changes_since(self, date_or_datetime)


    def export(self):
        """
        Provides the BIND (Berkeley Internet Name Domain) 9 formatted contents
        of the requested domain. This call is for a single domain only, and as such,
        does not provide subdomain information.

        Sample export:
            {u'accountId': 000000,
             u'contentType': u'BIND_9',
             u'contents': u'example.com.\t3600\tIN\tSOA\tns.rackspace.com. '
                'foo@example.com. 1354202974 21600 3600 1814400 500'
                'example.com.\t3600\tIN\tNS\tdns1.stabletransit.com.'
                'example.com.\t3600\tIN\tNS\tdns2.stabletransit.com.',
             u'id': 1111111}
        """
        return self.manager.export_domain(self)


    def update(self, emailAddress=None, ttl=None, comment=None):
        """
        Provides a way to modify the following attributes of a domain
        entry:
            - email address
            - ttl setting
            - comment
        """
        return self.manager.update_domain(self, emailAddress=emailAddress,
                ttl=ttl, comment=comment)


    def list_subdomains(self, limit=None, offset=None):
        """
        Returns a list of all subdomains for this domain.
        """
        return self.manager.list_subdomains(self, limit=limit, offset=offset)


    def list_records(self, limit=None, offset=None):
        """
        Returns a list of all records configured for this domain.
        """
        return self.manager.list_records(self, limit=limit, offset=offset)


    def search_records(self, record_type, name=None, data=None):
        """
        Returns a list of all records configured for this domain that match
        the supplied search criteria.
        """
        return self.manager.search_records(self, record_type=record_type,
                name=name, data=data)


    def find_record(self, record_type, name=None, data=None):
        """
        Returns a single record for this domain that matches the supplied
        search criteria.

        If no record matches, a DomainRecordNotFound exception will be raised.
        If more than one matches, a DomainRecordNotUnique exception will
        be raised.
        """
        matches = self.manager.search_records(self, record_type=record_type,
                name=name, data=data)
        if not matches:
            raise exc.DomainRecordNotFound
        elif len(matches) > 1:
            raise exc.DomainRecordNotUnique
        return matches[0]


    def add_records(self, records):
        """
        Adds the records to this domain. Each record should be a dict with the
        following keys:
            - type (required)
            - name (required)
            - data (required)
            - ttl (optional)
            - comment (optional)
            - priority (required for MX and SRV records; forbidden otherwise)
        """
        return self.manager.add_records(self, records)

    # Create an alias, so that adding a single record is more intuitive
    add_record = add_records


    def get_record(self, record):
        """
        Gets the full information for an existing record for this domain.
        """
        return self.manager.get_record(self, record)


    def update_record(self, record, data=None, priority=None,
            ttl=None, comment=None):
        """
        Modifies an existing record for this domain.
        """
        return self.manager.update_record(self, record, data=data,
                priority=priority, ttl=ttl, comment=comment)


    def delete_record(self, record):
        """
        Deletes an existing record for this domain.
        """
        return self.manager.delete_record(self, record)


class CloudDNSPTRRecord(object):
    """
    This represents a Cloud DNS PTR record (reverse DNS).
    """
    def __init__(self, data=None, device=None):
        self.type = self.id = self.data = self.name = None
        self.ttl = self.comment = None
        if data:
            for key, val in data.items():
                setattr(self, key, val)
        self.device = device


    def delete(self):
        """
        Deletes this PTR record from its device.
        """
        return pyos.cloud_dns.delete_ptr_records(self.device, self.data)


    def __repr__(self):
        reprkeys = ("id", "data", "name", "ttl")
        info = ", ".join("%s=%s" % (key, getattr(self, key)) for key in reprkeys)
        return "<%s %s>" % (self.__class__.__name__, info)



class CloudDNSManager(BaseManager):
    def __init__(self, api, resource_class=None, response_key=None,
            plural_response_key=None, uri_base=None):
        super(CloudDNSManager, self).__init__(api, resource_class=resource_class,
                response_key=response_key, plural_response_key=plural_response_key,
                uri_base=uri_base)
        self._paging = {"domain": {}, "subdomain": {}, "record": {}}
        self._reset_paging(service="all")
        self._timeout = DEFAULT_TIMEOUT
        self._delay = DEFAULT_DELAY


    def _create_body(self, name, emailAddress, ttl=3600, comment=None,
            subdomains=None, records=None):
        """
        Creates the appropriate dict for creating a new domain.
        """
        if subdomains is None:
            subdomains = []
        if records is None:
            records = []
        body = {"domains": [{
                "name": name,
                "emailAddress": emailAddress,
                "ttl": ttl,
                "comment": comment,
                "subdomains": {
                    "domains": subdomains
                    },
                "recordsList": {
                    "records": records
                    },
                }]}
        return body


    def _set_timeout(self, timeout):
        """
        Changes the duration for which the program will wait for a response from
        the DNS system. Setting the timeout to zero will make that program wait
        an indefinite amount of time.
        """
        self._timeout = timeout


    def _set_delay(self, delay):
        """
        Changes the interval that the program will pause in between attempts to
        see if a request has completed.
        """
        self._delay = delay


    def _reset_paging(self, service, body=None):
        """
        Resets the internal attributes when there is no current paging request.
        """
        if service == "all":
            for svc in self._paging.keys():
                svc_dct = self._paging[svc]
                svc_dct["next_uri"] = svc_dct["prev_uri"] = None
                svc_dct["total_entries"] = None
            return
        svc_dct = self._paging[service]
        svc_dct["next_uri"] = svc_dct["prev_uri"] = None
        svc_dct["total_entries"] = None
        if not body:
            return
        svc_dct["total_entries"] = body.get("totalEntries")
        links = body.get("links")
        uri_base = self.uri_base
        if links:
            for link in links:
                href = link["href"]
                pos = href.index(uri_base)
                page_uri = href[pos - 1:]
                if link["rel"] == "next":
                    svc_dct["next_uri"] = page_uri
                elif link["rel"] == "previous":
                    svc_dct["prev_uri"] = page_uri


    def _get_pagination_qs(self, limit, offset):
        pagination_items = []
        if limit is not None:
            pagination_items.append("limit=%s" % limit)
        if offset is not None:
            pagination_items.append("offset=%s" % offset)
        qs = "&".join(pagination_items)
        qs = "?%s" % qs if qs else ""
        return qs


    def list(self, limit=None, offset=None):
        """Gets a list of all domains, or optionally a page of domains."""
        uri = "/%s%s" % (self.uri_base, self._get_pagination_qs(limit, offset))
        return self._list(uri)


    def _list(self, uri, obj_class=None, list_all=False):
        """
        Handles the communication with the API when getting
        a full listing of the resources managed by this class.
        """
        resp, resp_body = self._retry_get(uri)
        if obj_class is None:
            obj_class = self.resource_class

        data = resp_body[self.plural_response_key]
        ret = [obj_class(self, res, loaded=False)
                for res in data if res]
        self._reset_paging("domain", resp_body)
        if list_all:
            dom_paging = self._paging.get("domain", {})
            while dom_paging.get("next_uri"):
                next_uri = dom_paging.get("next_uri")
                ret.extend(self._list(uri=next_uri, obj_class=obj_class,
                        list_all=False))
        return ret


    def list_previous_page(self):
        """
        When paging through results, this will return the previous page, using
        the same limit. If there are no more results, a NoMoreResults exception
        will be raised.
        """
        uri = self._paging.get("domain", {}).get("prev_uri")
        if uri is None:
            raise exc.NoMoreResults("There are no previous pages of domains "
                    "to list.")
        return self._list(uri)


    def list_next_page(self):
        """
        When paging through results, this will return the next page, using the
        same limit. If there are no more results, a NoMoreResults exception
        will be raised.
        """
        uri = self._paging.get("domain", {}).get("next_uri")
        if uri is None:
            raise exc.NoMoreResults("There are no more pages of domains to "
                    "list.")
        return self._list(uri)


    def _get(self, uri):
        """
        Handles the communication with the API when getting
        a specific resource managed by this class.

        Because DNS returns a different format for the body,
        the BaseManager method must be overridden here.
        """
        uri = "%s?showRecords=false&showSubdomains=false" % uri
        resp, body = self._retry_get(uri)
        body["records"] = []
        return self.resource_class(self, body, loaded=True)

    def _retry_get(self, uri):
        """
        Handles GET calls to the Cloud DNS API in order to retry on empty
        body responses.
        """
        for i in six.moves.range(DEFAULT_RETRY):
            resp, body = self.api.method_get(uri)
            if body:
                return resp, body
        # Tried too many times
        raise exc.ServiceResponseFailure("The Cloud DNS service failed to "
                "respond to the request.")

    def _async_call(self, uri, body=None, method="GET", error_class=None,
            has_response=True, *args, **kwargs):
        """
        Handles asynchronous call/responses for the DNS API.

        Returns the response headers and body if the call was successful.
        If an error status is returned, and the 'error_class' parameter is
        specified, that class of error will be raised with the details from
        the response. If no error class is specified, the response headers
        and body will be returned to the calling method, which will have
        to handle the result.
        """
        api_methods = {
                "GET": self._retry_get,
                "POST": self.api.method_post,
                "PUT": self.api.method_put,
                "DELETE": self.api.method_delete,
                }
        api_method = api_methods[method]
        if body is None:
            resp, resp_body = api_method(uri, *args, **kwargs)
        else:
            resp, resp_body = api_method(uri, body=body, *args, **kwargs)
        callbackURL = resp_body["callbackUrl"].split("/status/")[-1]
        massagedURL = "/status/%s?showDetails=true" % callbackURL
        start = time.time()
        timed_out = False
        while (resp_body["status"] == "RUNNING") and not timed_out:
            resp_body = None
            while resp_body is None and not timed_out:
                resp, resp_body = self._retry_get(massagedURL)
                if self._timeout:
                    timed_out = ((time.time() - start) > self._timeout)
                    time.sleep(self._delay)

        if timed_out:
            raise exc.DNSCallTimedOut("The API call to '%s' did not complete "
                    "after %s seconds." % (uri, self._timeout))
        if error_class and (resp_body["status"] == "ERROR"):
            # This call will handle raising the error.
            self._process_async_error(resp_body, error_class)
        if has_response:
            ret = resp, resp_body["response"]
        else:
            ret = resp, resp_body
        try:
            resp_body = json.loads(resp_body)
        except Exception:
            pass
        return ret


    def _process_async_error(self, resp_body, error_class):
        """
        The DNS API does not return a consistent format for their error
        messages. This abstracts out the differences in order to present
        a single unified message in the exception to be raised.
        """
        def _fmt_error(err):
            # Remove the cumbersome Java-esque message
            details = err.get("details", "").replace("\n", " ")
            if not details:
                details = err.get("message", "")
            return "%s (%s)" % (details, err.get("code", ""))

        error = resp_body.get("error", "")
        if "failedItems" in error:
            # Multi-error response
            faults = error.get("failedItems", {}).get("faults", [])
            msgs = [_fmt_error(fault) for fault in faults]
            msg = "\n".join(msgs)
        else:
            msg = _fmt_error(error)
        raise error_class(msg)


    def _create(self, uri, body, records=None, subdomains=None,
            return_none=False, return_raw=False, **kwargs):
        """
        Handles the communication with the API when creating a new
        resource managed by this class.

        Since DNS works completely differently for create() than the other
        APIs, this method overrides the default BaseManager behavior.

        If 'records' are supplied, they should be a list of dicts. Each
        record dict should have the following format:

            {"name": "example.com",
            "type": "A",
            "data": "192.0.2.17",
            "ttl": 86400}

        If 'subdomains' are supplied, they should be a list of dicts. Each
        subdomain dict should have the following format:

            {"name": "sub1.example.com",
             "comment": "1st sample subdomain",
             "emailAddress": "sample@rackspace.com"}
        """
        self.run_hooks("modify_body_for_create", body, **kwargs)
        resp, resp_body = self._async_call(uri, body=body, method="POST",
                error_class=exc.DomainCreationFailed)
        response_body = resp_body[self.response_key][0]
        return self.resource_class(self, response_body)


    def delete(self, domain, delete_subdomains=False):
        """
        Deletes the specified domain and all of its resource records. If the
        domain has subdomains, each subdomain will now become a root domain. If
        you wish to also delete any subdomains, pass True to 'delete_subdomains'.
        """
        uri = "/%s/%s" % (self.uri_base, utils.get_id(domain))
        if delete_subdomains:
            uri = "%s?deleteSubdomains=true" % uri
        resp, resp_body = self._async_call(uri, method="DELETE",
                error_class=exc.DomainDeletionFailed, has_response=False)


    def findall(self, **kwargs):
        """
        Finds all items with attributes matching ``**kwargs``.

        Normally this isn't very efficient, since the default action is to
        load the entire list and then filter on the Python side, but the DNS
        API provides a more efficient search option when filtering on name.
        So if the filter is on name, use that; otherwise, use the default.
        """
        if (len(kwargs) == 1) and ("name" in kwargs):
            # Filtering on name; use the more efficient method.
            nm = kwargs["name"].lower()
            uri = "/%s?name=%s" % (self.uri_base, nm)
            matches = self._list(uri, list_all=True)
            return [match for match in matches
                if match.name.lower() == nm]
        else:
            return super(CloudDNSManager, self).findall(**kwargs)


    def changes_since(self, domain, date_or_datetime):
        """
        Gets the changes for a domain since the specified date/datetime.
        The date can be one of:
            - a Python datetime object
            - a Python date object
            - a string in the format 'YYYY-MM-YY HH:MM:SS'
            - a string in the format 'YYYY-MM-YY'

        It returns a list of dicts, whose keys depend on the specific change
        that was made. A simple example of such a change dict:

            {u'accountId': 000000,
             u'action': u'update',
             u'changeDetails': [{u'field': u'serial_number',
               u'newValue': u'1354038941',
               u'originalValue': u'1354038940'},
              {u'field': u'updated_at',
               u'newValue': u'Tue Nov 27 17:55:41 UTC 2012',
               u'originalValue': u'Tue Nov 27 17:55:40 UTC 2012'}],
             u'domain': u'example.com',
             u'targetId': 00000000,
             u'targetType': u'Domain'}
        """
        domain_id = utils.get_id(domain)
        dt = utils.iso_time_string(date_or_datetime, show_tzinfo=True)
        uri = "/domains/%s/changes?since=%s" % (domain_id, dt)
        resp, body = self._retry_get(uri)
        return body.get("changes", [])


    def export_domain(self, domain):
        """
        Provides the BIND (Berkeley Internet Name Domain) 9 formatted contents
        of the requested domain. This call is for a single domain only, and as
        such, does not provide subdomain information.

        Sample export:
            {u'accountId': 000000,
             u'contentType': u'BIND_9',
             u'contents': u'example.com.\t3600\tIN\tSOA\tns.rackspace.com. '
                'foo@example.com. 1354202974 21600 3600 1814400 500'
                'example.com.\t3600\tIN\tNS\tdns1.stabletransit.com.'
                'example.com.\t3600\tIN\tNS\tdns2.stabletransit.com.',
             u'id': 1111111}
        """
        uri = "/domains/%s/export" % utils.get_id(domain)
        resp, resp_body = self._async_call(uri, method="GET",
                error_class=exc.NotFound)
        return resp_body.get("contents", "")


    def import_domain(self, domain_data):
        """
        Takes a string in the BIND 9 format and creates a new domain. See the
        'export_domain()' method for a description of the format.
        """
        uri = "/domains/import"
        body = {"domains": [{
                "contentType": "BIND_9",
                "contents": domain_data,
                }]}
        resp, resp_body = self._async_call(uri, method="POST", body=body,
                error_class=exc.DomainCreationFailed)
        return resp_body


    def update_domain(self, domain, emailAddress=None, ttl=None, comment=None):
        """
        Provides a way to modify the following attributes of a domain
        record:
            - email address
            - ttl setting
            - comment
        """
        if not any((emailAddress, ttl, comment)):
            raise exc.MissingDNSSettings(
                    "No settings provided to update_domain().")
        uri = "/domains/%s" % utils.get_id(domain)
        body = {"comment": comment,
                "ttl": ttl,
                "emailAddress": emailAddress,
                }
        none_keys = [key for key, val in body.items()
                if val is None]
        for none_key in none_keys:
            body.pop(none_key)
        resp, resp_body = self._async_call(uri, method="PUT", body=body,
                error_class=exc.DomainUpdateFailed, has_response=False)
        return resp_body


    def list_subdomains(self, domain, limit=None, offset=None):
        """
        Returns a list of all subdomains of the specified domain.
        """
        # The commented-out uri is the official API, but it is
        # horribly slow.
#        uri = "/domains/%s/subdomains" % utils.get_id(domain)
        uri = "/domains?name=%s" % domain.name
        page_qs = self._get_pagination_qs(limit, offset)
        if page_qs:
            uri = "%s&%s" % (uri, page_qs[1:])
        return self._list_subdomains(uri, domain.id)


    def _list_subdomains(self, uri, domain_id):
        resp, body = self._retry_get(uri)
        self._reset_paging("subdomain", body)
        subdomains = body.get("domains", [])
        return [CloudDNSDomain(self, subdomain, loaded=False)
                for subdomain in subdomains
                if subdomain["id"] != domain_id]


    def list_subdomains_previous_page(self):
        """
        When paging through subdomain results, this will return the previous
        page, using the same limit. If there are no more results, a
        NoMoreResults exception will be raised.
        """
        uri = self._paging.get("subdomain", {}).get("prev_uri")
        if uri is None:
            raise exc.NoMoreResults("There are no previous pages of subdomains "
                    "to list.")
        return self._list_subdomains(uri)


    def list_subdomains_next_page(self):
        """
        When paging through subdomain results, this will return the next page,
        using the same limit. If there are no more results, a NoMoreResults
        exception will be raised.
        """
        uri = self._paging.get("subdomain", {}).get("next_uri")
        if uri is None:
            raise exc.NoMoreResults("There are no more pages of subdomains "
                    "to list.")
        return self._list_subdomains(uri)


    def list_records(self, domain, limit=None, offset=None):
        """
        Returns a list of all records configured for the specified domain.
        """
        uri = "/domains/%s/records%s" % (utils.get_id(domain),
                self._get_pagination_qs(limit, offset))
        return self._list_records(uri)


    def _list_records(self, uri):
        resp, body = self._retry_get(uri)
        self._reset_paging("record", body)
        # The domain ID will be in the URL
        pat = "domains/([^/]+)/records"
        mtch = re.search(pat, uri)
        dom_id = mtch.groups()[0]
        records = body.get("records", [])
        for record in records:
            record["domain_id"] = dom_id
        return [CloudDNSRecord(self, record, loaded=False)
                for record in records if record]


    def list_records_previous_page(self):
        """
        When paging through record results, this will return the previous page,
        using the same limit. If there are no more results, a NoMoreResults
        exception will be raised.
        """
        uri = self._paging.get("record", {}).get("prev_uri")
        if uri is None:
            raise exc.NoMoreResults("There are no previous pages of records "
                    "to list.")
        return self._list_records(uri)


    def list_records_next_page(self):
        """
        When paging through record results, this will return the next page,
        using the same limit. If there are no more results, a NoMoreResults
        exception will be raised.
        """
        uri = self._paging.get("record", {}).get("next_uri")
        if uri is None:
            raise exc.NoMoreResults("There are no more pages of records to list.")
        return self._list_records(uri)


    def search_records(self, domain, record_type, name=None, data=None):
        """
        Returns a list of all records configured for the specified domain that
        match the supplied search criteria.
        """
        search_params = []
        if name:
            search_params.append("name=%s" % name)
        if data:
            search_params.append("data=%s" % data)
        query_string = "&".join(search_params)
        dom_id = utils.get_id(domain)
        uri = "/domains/%s/records?type=%s" % (dom_id, record_type)
        if query_string:
            uri = "%s&%s" % (uri, query_string)
        resp, body = self._retry_get(uri)
        records = body.get("records", [])
        self._reset_paging("record", body)
        rec_paging = self._paging.get("record", {})
        while rec_paging.get("next_uri"):
            resp, body = self._retry_get(rec_paging.get("next_uri"))
            self._reset_paging("record", body)
            records.extend(body.get("records", []))
        for record in records:
            record["domain_id"] = dom_id
        return [CloudDNSRecord(self, record, loaded=False)
                for record in records if record]


    def add_records(self, domain, records):
        """
        Adds the records to this domain. Each record should be a dict with the
        following keys:
            - type (required)
            - name (required)
            - data (required)
            - ttl (optional)
            - comment (optional)
            - priority (required for MX and SRV records; forbidden otherwise)
        """
        if isinstance(records, dict):
            # Single record passed
            records = [records]
        dom_id = utils.get_id(domain)
        uri = "/domains/%s/records" % dom_id
        body = {"records": records}
        resp, resp_body = self._async_call(uri, method="POST", body=body,
                error_class=exc.DomainRecordAdditionFailed, has_response=False)
        records = resp_body.get("response", {}).get("records", [])
        for record in records:
            record["domain_id"] = dom_id
        return [CloudDNSRecord(self, record, loaded=False)
                for record in records if record]


    def get_record(self, domain, record):
        """
        Gets the full information for an existing record for this domain.
        """
        rec_id = utils.get_id(record)
        domain_id = utils.get_id(domain)
        uri = "/domains/%s/records/%s" % (domain_id, rec_id)
        resp, resp_body = self._retry_get(uri)
        resp_body['domain_id'] = domain_id
        return CloudDNSRecord(self, resp_body, loaded=False)


    def update_record(self, domain, record, data=None, priority=None,
            ttl=None, comment=None):
        """
        Modifies an existing record for a domain.
        """
        rec_id = utils.get_id(record)
        uri = "/domains/%s/records/%s" % (utils.get_id(domain), rec_id)
        body = {"name": record.name}
        all_opts = (("data", data), ("priority", priority), ("ttl", ttl),
                ("comment", comment))
        opts = [(k, v) for k, v in all_opts if v is not None]
        body.update(dict(opts))
        resp, resp_body = self._async_call(uri, method="PUT", body=body,
                error_class=exc.DomainRecordUpdateFailed, has_response=False)
        return resp_body


    def delete_record(self, domain, record):
        """
        Deletes an existing record for a domain.
        """
        uri = "/domains/%s/records/%s" % (utils.get_id(domain),
                utils.get_id(record))
        resp, resp_body = self._async_call(uri, method="DELETE",
                error_class=exc.DomainRecordDeletionFailed, has_response=False)
        return resp_body


    def _get_ptr_details(self, device, device_type):
        """
        Takes a device and device type and returns the corresponding HREF link
        and service name for use with PTR record management.
        """
        context = self.api.identity
        region = self.api.region_name
        if device_type.lower().startswith("load"):
            ep = pyos._get_service_endpoint(context, "load_balancer", region)
            svc = "loadbalancers"
            svc_name = "cloudLoadBalancers"
        else:
            ep = pyos._get_service_endpoint(context, "compute", region)
            svc = "servers"
            svc_name = "cloudServersOpenStack"
        href = "%s/%s/%s" % (ep, svc, utils.get_id(device))
        return (href, svc_name)


    def _resolve_device_type(self, device):
        """
        Given a device, determines if it is a CloudServer, a CloudLoadBalancer,
        or an invalid device.
        """
        try:
            from tests.unit import fakes
            server_types = (pyos.CloudServer, fakes.FakeServer)
            lb_types = (CloudLoadBalancer, fakes.FakeLoadBalancer,
                    fakes.FakeDNSDevice)
        except ImportError:
            # Not running with tests
            server_types = (pyos.CloudServer, )
            lb_types = (CloudLoadBalancer, )
        if isinstance(device, server_types):
            device_type = "server"
        elif isinstance(device, lb_types):
            device_type = "loadbalancer"
        else:
            raise exc.InvalidDeviceType("The device '%s' must be a CloudServer "
                    "or a CloudLoadBalancer." % device)
        return device_type


    def list_ptr_records(self, device):
        """
        Returns a list of all PTR records configured for this device.
        """
        device_type = self._resolve_device_type(device)
        href, svc_name = self._get_ptr_details(device, device_type)
        uri = "/rdns/%s?href=%s" % (svc_name, href)
        try:
            resp, resp_body = self._retry_get(uri)
        except exc.NotFound:
            return []
        records = [CloudDNSPTRRecord(rec, device)
                for rec in resp_body.get("records", [])]
        return records


    def add_ptr_records(self, device, records):
        """
        Adds one or more PTR records to the specified device.
        """
        device_type = self._resolve_device_type(device)
        href, svc_name = self._get_ptr_details(device, device_type)
        if not isinstance(records, (list, tuple)):
            records = [records]
        body = {"recordsList": {
                    "records": records},
                "link": {
                    "content": "",
                    "href": href,
                    "rel": svc_name,
                }}
        uri = "/rdns"
        # This is a necessary hack, so here's why: if you attempt to add
        # PTR records to device, and you don't have rights to either the device
        # or the IP address, the DNS API will return a 401 - Unauthorized.
        # Unfortunately, the pyos client interprets this as a bad auth token,
        # and there is no way to distinguish this from an actual authentication
        # failure. The client will attempt to re-authenticate as a result, and
        # will fail, due to the DNS API not having regional endpoints. The net
        # result is that an EndpointNotFound exception will be raised, which
        # we catch here and then raise a more meaningful exception.
        # The Rackspace DNS team is working on changing this to return a 403
        # instead; when that happens this kludge can go away.
        try:
            resp, resp_body = self._async_call(uri, body=body, method="POST",
                    error_class=exc.PTRRecordCreationFailed)
        except exc.EndpointNotFound:
            raise exc.InvalidPTRRecord("The domain/IP address information is not "
                    "valid for this device.")
        return resp_body.get("records")
        records = [CloudDNSPTRRecord(rec, device)
                for rec in resp_body.get("records", [])]
        return records


    def update_ptr_record(self, device, record, domain_name, data=None,
            ttl=None, comment=None):
        """
        Updates a PTR record with the supplied values.
        """
        device_type = self._resolve_device_type(device)
        href, svc_name = self._get_ptr_details(device, device_type)
        try:
            rec_id = record.id
        except AttributeError:
            rec_id = record
        rec = {"name": domain_name,
              "id": rec_id,
              "type": "PTR",
              "data": data,
            }
        if ttl is not None:
            # Minimum TTL is 300 seconds
            rec["ttl"] = max(300, ttl)
        if comment is not None:
            # Maximum comment length is 160 chars
            rec["comment"] = comment[:160]
        body = {"recordsList": {
                    "records": [rec]},
                "link": {
                    "content": "",
                    "href": href,
                    "rel": svc_name,
                }}
        uri = "/rdns"
        try:
            resp, resp_body = self._async_call(uri, body=body, method="PUT",
                    has_response=False, error_class=exc.PTRRecordUpdateFailed)
        except exc.EndpointNotFound as e:
            raise exc.InvalidPTRRecord("The record domain/IP address "
                    "information is not valid for this device.")
        return resp_body.get("status") == "COMPLETED"


    def delete_ptr_records(self, device, ip_address=None):
        """
        Deletes the PTR records for the specified device. If 'ip_address' is
        supplied, only the PTR records with that IP address will be deleted.
        """
        device_type = self._resolve_device_type(device)
        href, svc_name = self._get_ptr_details(device, device_type)
        uri = "/rdns/%s?href=%s" % (svc_name, href)
        if ip_address:
            uri = "%s&ip=%s" % (uri, ip_address)
        resp, resp_body = self._async_call(uri, method="DELETE",
                has_response=False,
                error_class=exc.PTRRecordDeletionFailed)
        return resp_body.get("status") == "COMPLETED"



class CloudDNSClient(BaseClient):
    """
    This is the primary class for interacting with Cloud DNS.
    """
    name = "Cloud DNS"

    def _configure_manager(self):
        """
        Creates a manager to handle the instances, and another
        to handle flavors.
        """
        self._manager = CloudDNSManager(self, resource_class=CloudDNSDomain,
                response_key="domains", plural_response_key="domains",
                uri_base="domains")

    def method_get(self, uri, **kwargs):
        """
        Overload the method_get function in order to retry on empty body
        responses from the Cloud DNS API
        """
        for i in six.moves.range(3):
            resp, body = super(CloudDNSClient, self).method_get(uri, **kwargs)
            if body:
                return resp, body
        raise exc.ServiceResponseFailure("The Cloud DNS service failed to "
                "respond to the request.")


    def set_timeout(self, timeout):
        """
        Sets the amount of time that calls will wait for a response from
        the DNS system before timing out. Setting the timeout to zero will
        cause execution to wait indefinitely until the call completes.
        """
        self._manager._set_timeout(timeout)


    def set_delay(self, delay):
        """
        Changes the interval that the program will pause in between attempts to
        see if a request has completed.
        """
        self._manager._set_delay(delay)


    def list(self, limit=None, offset=None):
        """Returns a list of all resources."""
        return self._manager.list(limit=limit, offset=offset)


    def list_previous_page(self):
        """Returns the previous page of results."""
        return self._manager.list_previous_page()


    def list_next_page(self):
        """Returns the next page of results."""
        return self._manager.list_next_page()


    def get_domain_iterator(self):
        """
        Returns an iterator that will return each available domain. If there are
        more than the limit of 100 domains, the iterator will continue to fetch
        domains from the API until all domains have been returned.
        """
        return DomainResultsIterator(self._manager)


    @assure_domain
    def changes_since(self, domain, date_or_datetime):
        """
        Gets the changes for a domain since the specified date/datetime.
        The date can be one of:
            - a Python datetime object
            - a Python date object
            - a string in the format 'YYYY-MM-YY HH:MM:SS'
            - a string in the format 'YYYY-MM-YY'

        It returns a list of dicts, whose keys depend on the specific change
        that was made. A simple example of such a change dict:

            {u'accountId': 000000,
             u'action': u'update',
             u'changeDetails': [{u'field': u'serial_number',
               u'newValue': u'1354038941',
               u'originalValue': u'1354038940'},
              {u'field': u'updated_at',
               u'newValue': u'Tue Nov 27 17:55:41 UTC 2012',
               u'originalValue': u'Tue Nov 27 17:55:40 UTC 2012'}],
             u'domain': u'example.com',
             u'targetId': 00000000,
             u'targetType': u'Domain'}
        """
        return domain.changes_since(date_or_datetime)


    @assure_domain
    def export_domain(self, domain):
        """
        Provides the BIND (Berkeley Internet Name Domain) 9 formatted contents
        of the requested domain. This call is for a single domain only, and as
        such, does not provide subdomain information.

        Sample export:

            {u'accountId': 000000,
             u'contentType': u'BIND_9',
             u'contents': u'example.com.\t3600\tIN\tSOA\tns.rackspace.com. '
                'foo@example.com. 1354202974 21600 3600 1814400 500'
                'example.com.\t3600\tIN\tNS\tdns1.stabletransit.com.'
                'example.com.\t3600\tIN\tNS\tdns2.stabletransit.com.',
             u'id': 1111111}
        """
        return domain.export()


    def import_domain(self, domain_data):
        """
        Takes a string in the BIND 9 format and creates a new domain. See the
        'export_domain()' method for a description of the format.
        """
        return self._manager.import_domain(domain_data)


    @assure_domain
    def update_domain(self, domain, emailAddress=None, ttl=None, comment=None):
        """
        Provides a way to modify the following attributes of a domain
        record:
            - email address
            - ttl setting
            - comment
        """
        return domain.update(emailAddress=emailAddress,
                ttl=ttl, comment=comment)


    @assure_domain
    def delete(self, domain, delete_subdomains=False):
        """
        Deletes the specified domain and all of its resource records. If the
        domain has subdomains, each subdomain will now become a root domain. If
        you wish to also delete any subdomains, pass True to 'delete_subdomains'.
        """
        domain.delete(delete_subdomains=delete_subdomains)


    @assure_domain
    def list_subdomains(self, domain, limit=None, offset=None):
        """
        Returns a list of all subdomains for the specified domain.
        """
        return domain.list_subdomains(limit=limit, offset=offset)


    def get_subdomain_iterator(self, domain, limit=None, offset=None):
        """
        Returns an iterator that will return each available subdomain for the
        specified domain. If there are more than the limit of 100 subdomains,
        the iterator will continue to fetch subdomains from the API until all
        subdomains have been returned.
        """
        return SubdomainResultsIterator(self._manager, domain=domain)


    def list_subdomains_previous_page(self):
        """Returns the previous page of subdomain results."""
        return self._manager.list_subdomains_previous_page()


    def list_subdomains_next_page(self):
        """Returns the next page of subdomain results."""
        return self._manager.list_subdomains_next_page()


    @assure_domain
    def list_records(self, domain, limit=None, offset=None):
        """
        Returns a list of all records configured for the specified domain.
        """
        return domain.list_records(limit=limit, offset=offset)


    def get_record_iterator(self, domain):
        """
        Returns an iterator that will return each available DNS record for the
        specified domain. If there are more than the limit of 100 records, the
        iterator will continue to fetch records from the API until all records
        have been returned.
        """
        return RecordResultsIterator(self._manager, domain=domain)


    def list_records_previous_page(self):
        """Returns the previous page of record results."""
        return self._manager.list_records_previous_page()


    def list_records_next_page(self):
        """Returns the next page of record results."""
        return self._manager.list_records_next_page()


    @assure_domain
    def search_records(self, domain, record_type, name=None, data=None):
        """
        Returns a list of all records configured for the specified domain
        that match the supplied search criteria.
        """
        return domain.search_records(record_type=record_type,
                name=name, data=data)


    @assure_domain
    def find_record(self, domain, record_type, name=None, data=None):
        """
        Returns a single record for this domain that matches the supplied
        search criteria.

        If no record matches, a DomainRecordNotFound exception will be raised.
        If more than one matches, a DomainRecordNotUnique exception will
        be raised.
        """
        return domain.find_record(record_type=record_type,
                name=name, data=data)


    @assure_domain
    def add_records(self, domain, records):
        """
        Adds the records to this domain. Each record should be a dict with the
        following keys:
            - type (required)
            - name (required)
            - data (required)
            - ttl (optional)
            - comment (optional)
            - priority (required for MX and SRV records; forbidden otherwise)
        """
        return domain.add_records(records)

    # Create an alias, so that adding a single record is more intuitive
    add_record = add_records


    @assure_domain
    def get_record(self, domain, record):
        """
        Gets the full information for an existing record or record ID for the
        specified domain.
        """
        return domain.get_record(record)


    @assure_domain
    def update_record(self, domain, record, data=None, priority=None,
            ttl=None, comment=None):
        """
        Modifies an existing record for a domain.
        """
        return domain.update_record(record, data=data,
                priority=priority, ttl=ttl, comment=comment)


    @assure_domain
    def delete_record(self, domain, record):
        """
        Deletes an existing record for this domain.
        """
        return domain.delete_record(record)


    def list_ptr_records(self, device):
        """
        Returns a list of all PTR records configured for this device.
        """
        return self._manager.list_ptr_records(device)


    def add_ptr_records(self, device, records):
        """
        Adds one or more PTR records to the specified device.
        """
        return self._manager.add_ptr_records(device, records)


    def update_ptr_record(self, device, record, domain_name, data=None,
            ttl=None, comment=None):
        """
        Updates a PTR record with the supplied values.
        """
        return self._manager.update_ptr_record(device, record, domain_name,
                data=data, ttl=ttl, comment=comment)


    def delete_ptr_records(self, device, ip_address=None):
        """
        Deletes the PTR records for the specified device. If 'ip_address'
        is supplied, only the PTR records with that IP address will be deleted.
        """
        return self._manager.delete_ptr_records(device, ip_address=ip_address)


    def get_absolute_limits(self):
        """
        Returns a dict with the absolute limits for the current account.
        """
        resp, body = self.method_get("/limits")
        absolute_limits = body.get("limits", {}).get("absolute")
        return absolute_limits


    def get_rate_limits(self):
        """
        Returns a dict with the current rate limit information for domain
        and status requests.
        """
        resp, body = self.method_get("/limits")
        rate_limits = body.get("limits", {}).get("rate")
        ret = []
        for rate_limit in rate_limits:
            limits = rate_limit["limit"]
            uri_limits = {"uri": rate_limit["uri"],
                    "limits": limits}
            ret.append(uri_limits)
        return ret



class ResultsIterator(object):
    """
    This object will iterate over all the results for a given
    type of listing, no matter how many items exist.

    This is an abstract class; subclasses must define the
    _init_methods() method.
    """
    def __init__(self, manager, domain=None):
        self.manager = manager
        self.domain = domain
        self.domain_id = utils.get_id(domain) if domain else None
        self.results = []
        self.next_uri = ""
        self.extra_args = tuple()
        self._init_methods()


    def _init_methods(self):
        """
        Must be implemented in subclasses.
        """
        raise NotImplementedError()


    def __iter__(self):
        return self


    def next(self):
        """
        Return the next available item. If there are no more items in the
        local 'results' list, check if there is a 'next_uri' value. If so,
        use that to get the next page of results from the API, and return
        the first item from that query.
        """
        try:
            return self.results.pop(0)
        except IndexError:
            if self.next_uri is None:
                raise StopIteration()
            else:
                if not self.next_uri:
                    if self.domain:
                        self.results = self.list_method(self.domain)
                    else:
                        self.results = self.list_method()
                else:
                    args = self.extra_args
                    self.results = self._list_method(self.next_uri, *args)
                self.next_uri = self.manager._paging.get(
                        self.paging_service, {}).get("next_uri")
        # We should have more results.
        try:
            return self.results.pop(0)
        except IndexError:
            raise StopIteration()


class DomainResultsIterator(ResultsIterator):
    """
    ResultsIterator subclass for iterating over all domains.
    """
    def _init_methods(self):
        self.list_method = self.manager.list
        self._list_method = self.manager._list
        self.paging_service = "domain"


class SubdomainResultsIterator(ResultsIterator):
    """
    ResultsIterator subclass for iterating over all subdomains.
    """
    def _init_methods(self):
        self.list_method = self.manager.list_subdomains
        self._list_method = self.manager._list_subdomains
        self.extra_args = (self.domain_id, )
        self.paging_service = "subdomain"


class RecordResultsIterator(ResultsIterator):
    """
    ResultsIterator subclass for iterating over all domain records.
    """
    def _init_methods(self):
        self.list_method = self.manager.list_records
        self._list_method = self.manager._list_records
        self.paging_service = "record"
