# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 datetime import datetime

try:
    import simplejson as json
except ImportError:
    import json

from libcloud.utils.py3 import httplib
from libcloud.utils.misc import reverse_dict
from libcloud.loadbalancer.base import LoadBalancer, Member, Driver, Algorithm
from libcloud.loadbalancer.base import DEFAULT_ALGORITHM
from libcloud.compute.drivers.rackspace import RackspaceConnection
from libcloud.common.types import LibcloudError
from libcloud.common.base import JsonResponse, PollingConnection
from libcloud.loadbalancer.types import State, MemberCondition
from libcloud.common.openstack import OpenStackDriverMixin
from libcloud.common.rackspace import AUTH_URL

ENDPOINT_ARGS_MAP = {
    'dfw': {'service_type': 'rax:load-balancer',
            'name': 'cloudLoadBalancers',
            'region': 'DFW'},
    'ord': {'service_type': 'rax:load-balancer',
            'name': 'cloudLoadBalancers',
            'region': 'ORD'},
    'iad': {'service_type': 'rax:load-balancer',
            'name': 'cloudLoadBalancers',
            'region': 'IAD'},
    'lon': {'service_type': 'rax:load-balancer',
            'name': 'cloudLoadBalancers',
            'region': 'LON'},
    'syd': {'service_type': 'rax:load-balancer',
            'name': 'cloudLoadBalancers',
            'region': 'SYD'},
    'hkg': {'service_type': 'rax:load-balancer',
            'name': 'cloudLoadBalancers',
            'region': 'HKG'},

}


class RackspaceResponse(JsonResponse):
    def parse_body(self):
        if not self.body:
            return None
        return super(RackspaceResponse, self).parse_body()

    def success(self):
        return 200 <= int(self.status) <= 299


class RackspaceHealthMonitor(object):
    """
    :param type: type of load balancer.  currently CONNECT (connection
                 monitoring), HTTP, HTTPS (connection and HTTP
                 monitoring) are supported.
    :type type: ``str``

    :param delay: minimum seconds to wait before executing the health
                      monitor.  (Must be between 1 and 3600)
    :type delay: ``int``

    :param timeout: maximum seconds to wait when establishing a
                    connection before timing out.  (Must be between 1
                    and 3600)
    :type timeout: ``int``

    :param attempts_before_deactivation: Number of monitor failures
                                         before removing a node from
                                         rotation. (Must be between 1
                                         and 10)
    :type attempts_before_deactivation: ``int``
    """

    def __init__(self, type, delay, timeout, attempts_before_deactivation):
        self.type = type
        self.delay = delay
        self.timeout = timeout
        self.attempts_before_deactivation = attempts_before_deactivation

    def __repr__(self):
        return ('<RackspaceHealthMonitor: type=%s, delay=%d, timeout=%d, '
                'attempts_before_deactivation=%d>' %
                (self.type, self.delay, self.timeout,
                 self.attempts_before_deactivation))

    def _to_dict(self):
        return {
            'type': self.type,
            'delay': self.delay,
            'timeout': self.timeout,
            'attemptsBeforeDeactivation': self.attempts_before_deactivation
        }


class RackspaceHTTPHealthMonitor(RackspaceHealthMonitor):
    """
    A HTTP health monitor adds extra features to a Rackspace health monitor.

    :param path: the HTTP path to monitor.
    :type path: ``str``

    :param body_regex: Regular expression used to evaluate the body of
                       the HTTP response.
    :type body_regex: ``str``

    :param status_regex: Regular expression used to evaluate the HTTP
                         status code of the response.
    :type status_regex: ``str``
    """

    def __init__(self, type, delay, timeout, attempts_before_deactivation,
                 path, body_regex, status_regex):
        super(RackspaceHTTPHealthMonitor, self).__init__(
            type, delay, timeout, attempts_before_deactivation)
        self.path = path
        self.body_regex = body_regex
        self.status_regex = status_regex

    def __repr__(self):
        return ('<RackspaceHTTPHealthMonitor: type=%s, delay=%d, timeout=%d, '
                'attempts_before_deactivation=%d, path=%s, body_regex=%s, '
                'status_regex=%s>' %
                (self.type, self.delay, self.timeout,
                 self.attempts_before_deactivation, self.path, self.body_regex,
                 self.status_regex))

    def _to_dict(self):
        super_dict = super(RackspaceHTTPHealthMonitor, self)._to_dict()
        super_dict['path'] = self.path
        super_dict['statusRegex'] = self.status_regex

        if self.body_regex:
            super_dict['bodyRegex'] = self.body_regex

        return super_dict


class RackspaceConnectionThrottle(object):
    """
    :param min_connections: Minimum number of connections per IP address
                            before applying throttling.
    :type min_connections: ``int``

    :param max_connections: Maximum number of connections per IP address.
                            (Must be between 0 and 100000, 0 allows an
                            unlimited number of connections.)
    :type max_connections: ``int``

    :param max_connection_rate: Maximum number of connections allowed
                                from a single IP address within the
                                given rate_interval_seconds.  (Must be
                                between 0 and 100000, 0 allows an
                                unlimited number of connections.)
    :type max_connection_rate: ``int``

    :param rate_interval_seconds: Interval at which the
                                  max_connection_rate is enforced.
                                  (Must be between 1 and 3600.)
    :type rate_interval_seconds: ``int``
    """

    def __init__(self, min_connections, max_connections,
                 max_connection_rate, rate_interval_seconds):
        self.min_connections = min_connections
        self.max_connections = max_connections
        self.max_connection_rate = max_connection_rate
        self.rate_interval_seconds = rate_interval_seconds

    def __repr__(self):
        return ('<RackspaceConnectionThrottle: min_connections=%d, '
                'max_connections=%d, max_connection_rate=%d, '
                'rate_interval_seconds=%d>' %
                (self.min_connections, self.max_connections,
                 self.max_connection_rate, self.rate_interval_seconds))

    def _to_dict(self):
        return {
            'maxConnections': self.max_connections,
            'minConnections': self.min_connections,
            'maxConnectionRate': self.max_connection_rate,
            'rateInterval': self.rate_interval_seconds
        }


class RackspaceAccessRuleType(object):
    ALLOW = 0
    DENY = 1

    _RULE_TYPE_STRING_MAP = {
        ALLOW: 'ALLOW',
        DENY: 'DENY'
    }


class RackspaceAccessRule(object):
    """
    An access rule allows or denies traffic to a Load Balancer based on the
    incoming IPs.

    :param id: Unique identifier to refer to this rule by.
    :type id: ``str``

    :param rule_type: RackspaceAccessRuleType.ALLOW or
                      RackspaceAccessRuleType.DENY.
    :type id: ``int``

    :param address: IP address or cidr (can be IPv4 or IPv6).
    :type address: ``str``
    """

    def __init__(self, id=None, rule_type=None, address=None):
        self.id = id
        self.rule_type = rule_type
        self.address = address

    def _to_dict(self):
        type_string =\
            RackspaceAccessRuleType._RULE_TYPE_STRING_MAP[self.rule_type]

        as_dict = {
            'type': type_string,
            'address': self.address
        }

        if self.id is not None:
            as_dict['id'] = self.id

        return as_dict


class RackspaceConnection(RackspaceConnection, PollingConnection):
    responseCls = RackspaceResponse
    auth_url = AUTH_URL
    poll_interval = 2
    timeout = 80
    cache_busting = True

    def request(self, action, params=None, data='', headers=None,
                method='GET'):
        if not headers:
            headers = {}
        if not params:
            params = {}

        if method in ('POST', 'PUT'):
            headers['Content-Type'] = 'application/json'

        return super(RackspaceConnection, self).request(
            action=action, params=params,
            data=data, method=method, headers=headers)

    def get_poll_request_kwargs(self, response, context, request_kwargs):
        return {'action': request_kwargs['action'],
                'method': 'GET'}

    def has_completed(self, response):
        state = response.object['loadBalancer']['status']
        if state == 'ERROR':
            raise LibcloudError("Load balancer entered an ERROR state.",
                                driver=self.driver)

        return state == 'ACTIVE'

    def encode_data(self, data):
        return data


class RackspaceLBDriver(Driver, OpenStackDriverMixin):
    connectionCls = RackspaceConnection
    api_name = 'rackspace_lb'
    name = 'Rackspace LB'
    website = 'http://www.rackspace.com/'

    LB_STATE_MAP = {
        'ACTIVE': State.RUNNING,
        'BUILD': State.PENDING,
        'ERROR': State.ERROR,
        'DELETED': State.DELETED,
        'PENDING_UPDATE': State.PENDING,
        'PENDING_DELETE': State.PENDING
    }

    LB_MEMBER_CONDITION_MAP = {
        'ENABLED': MemberCondition.ENABLED,
        'DISABLED': MemberCondition.DISABLED,
        'DRAINING': MemberCondition.DRAINING
    }

    CONDITION_LB_MEMBER_MAP = reverse_dict(LB_MEMBER_CONDITION_MAP)

    _VALUE_TO_ALGORITHM_MAP = {
        'RANDOM': Algorithm.RANDOM,
        'ROUND_ROBIN': Algorithm.ROUND_ROBIN,
        'LEAST_CONNECTIONS': Algorithm.LEAST_CONNECTIONS,
        'WEIGHTED_ROUND_ROBIN': Algorithm.WEIGHTED_ROUND_ROBIN,
        'WEIGHTED_LEAST_CONNECTIONS': Algorithm.WEIGHTED_LEAST_CONNECTIONS
    }

    _ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP)

    def __init__(self, key, secret=None, secure=True, host=None, port=None,
                 region='ord', **kwargs):
        ex_force_region = kwargs.pop('ex_force_region', None)
        if ex_force_region:
            # For backward compatibility
            region = ex_force_region
        OpenStackDriverMixin.__init__(self, **kwargs)
        super(RackspaceLBDriver, self).__init__(key=key, secret=secret,
                                                secure=secure, host=host,
                                                port=port, region=region)

    @classmethod
    def list_regions(cls):
        return ENDPOINT_ARGS_MAP.keys()

    def _ex_connection_class_kwargs(self):
        endpoint_args = ENDPOINT_ARGS_MAP[self.region]
        kwargs = self.openstack_connection_kwargs()
        kwargs['get_endpoint_args'] = endpoint_args
        return kwargs

    def list_protocols(self):
        return self._to_protocols(
            self.connection.request('/loadbalancers/protocols').object)

    def ex_list_protocols_with_default_ports(self):
        """
        List protocols with default ports.

        :rtype: ``list`` of ``tuple``
        :return: A list of protocols with default ports included.
        """
        return self._to_protocols_with_default_ports(
            self.connection.request('/loadbalancers/protocols').object)

    def list_balancers(self, ex_member_address=None, ex_status=None,
                       ex_changes_since=None, ex_params={}):
        """
        @inherits: :class:`Driver.list_balancers`

        :param ex_member_address: Optional IP address of the attachment member.
                                  If provided, only the load balancers which
                                  have this member attached will be returned.
        :type ex_member_address: ``str``

        :param ex_status: Optional. Filter balancers by status
        :type ex_status: ``str``

        :param ex_changes_since: Optional. List all load balancers that have
                                 changed since the specified date/time
        :type ex_changes_since: ``str``

        :param ex_params: Optional. Set parameters to be submitted to the API
                          in the query string
        :type ex_params: ``dict``
        """

        params = {}

        if ex_member_address:
            params['nodeaddress'] = ex_member_address

        if ex_status:
            params['status'] = ex_status

        if ex_changes_since:
            params['changes-since'] = ex_changes_since

        for key, value in ex_params.items():
            params[key] = value

        return self._to_balancers(
            self.connection.request('/loadbalancers', params=params).object)

    def create_balancer(self, name, members, protocol='http',
                        port=80, algorithm=DEFAULT_ALGORITHM):
        return self.ex_create_balancer(name, members, protocol, port,
                                       algorithm)

    def ex_create_balancer(self, name, members, protocol='http',
                           port=80, algorithm=DEFAULT_ALGORITHM, vip='PUBLIC'):
        """
        Creates a new load balancer instance

        :param name: Name of the new load balancer (required)
        :type  name: ``str``

        :param members: ``list`` of:class:`Member`s to attach to balancer
        :type  members: ``list`` of :class:`Member`

        :param protocol: Loadbalancer protocol, defaults to http.
        :type  protocol: ``str``

        :param port: Port the load balancer should listen on, defaults to 80
        :type  port: ``str``

        :param algorithm: Load balancing algorithm, defaults to
                            LBAlgorithm.ROUND_ROBIN
        :type  algorithm: :class:`Algorithm`

        :param vip: Virtual ip type of PUBLIC, SERVICENET, or ID of a virtual
                      ip
        :type  vip: ``str``

        :rtype: :class:`LoadBalancer`
        """
        balancer_attrs = self._kwargs_to_mutable_attrs(
            name=name,
            protocol=protocol,
            port=port,
            algorithm=algorithm,
            vip=vip)

        balancer_attrs.update({
            'nodes': [self._member_attributes(member) for member in members],
        })
        # balancer_attrs['nodes'] = ['fu']
        balancer_object = {"loadBalancer": balancer_attrs}

        resp = self.connection.request('/loadbalancers',
                                       method='POST',
                                       data=json.dumps(balancer_object))
        return self._to_balancer(resp.object['loadBalancer'])

    def _member_attributes(self, member):
        member_attributes = {'address': member.ip,
                             'port': member.port}

        member_attributes.update(self._kwargs_to_mutable_member_attrs(
            **member.extra))

        # If the condition is not specified on the member, then it should be
        # set to ENABLED by default
        if 'condition' not in member_attributes:
            member_attributes['condition'] =\
                self.CONDITION_LB_MEMBER_MAP[MemberCondition.ENABLED]

        return member_attributes

    def destroy_balancer(self, balancer):
        uri = '/loadbalancers/%s' % (balancer.id)
        resp = self.connection.request(uri, method='DELETE')

        return resp.status == httplib.ACCEPTED

    def ex_destroy_balancers(self, balancers):
        """
        Destroys a list of Balancers (the API supports up to 10).

        :param balancers: A list of Balancers to destroy.
        :type balancers: ``list`` of :class:`LoadBalancer`

        :return: Returns whether the destroy request was accepted.
        :rtype: ``bool``
        """
        ids = [('id', balancer.id) for balancer in balancers]
        resp = self.connection.request('/loadbalancers',
                                       method='DELETE',
                                       params=ids)

        return resp.status == httplib.ACCEPTED

    def get_balancer(self, balancer_id):
        uri = '/loadbalancers/%s' % (balancer_id)
        resp = self.connection.request(uri)

        return self._to_balancer(resp.object["loadBalancer"])

    def balancer_attach_member(self, balancer, member):
        member_object = {"nodes": [self._member_attributes(member)]}

        uri = '/loadbalancers/%s/nodes' % (balancer.id)
        resp = self.connection.request(uri, method='POST',
                                       data=json.dumps(member_object))
        return self._to_members(resp.object, balancer)[0]

    def ex_balancer_attach_members(self, balancer, members):
        """
        Attaches a list of members to a load balancer.

        :param balancer: The Balancer to which members will be attached.
        :type  balancer: :class:`LoadBalancer`

        :param members: A list of Members to attach.
        :type  members: ``list`` of :class:`Member`

        :rtype: ``list`` of :class:`Member`
        """
        member_objects = {"nodes": [self._member_attributes(member) for member
                                    in members]}

        uri = '/loadbalancers/%s/nodes' % (balancer.id)
        resp = self.connection.request(uri, method='POST',
                                       data=json.dumps(member_objects))
        return self._to_members(resp.object, balancer)

    def balancer_detach_member(self, balancer, member):
        # Loadbalancer always needs to have at least 1 member.
        # Last member cannot be detached. You can only disable it or destroy
        # the balancer.
        uri = '/loadbalancers/%s/nodes/%s' % (balancer.id, member.id)
        resp = self.connection.request(uri, method='DELETE')

        return resp.status == httplib.ACCEPTED

    def ex_balancer_detach_members(self, balancer, members):
        """
        Detaches a list of members from a balancer (the API supports up to 10).
        This method blocks until the detach request has been processed and the
        balancer is in a RUNNING state again.

        :param balancer: The Balancer to detach members from.
        :type  balancer: :class:`LoadBalancer`

        :param members: A list of Members to detach.
        :type  members: ``list`` of :class:`Member`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        accepted = self.ex_balancer_detach_members_no_poll(balancer, members)

        if not accepted:
            msg = 'Detach members request was not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_balancer_detach_members_no_poll(self, balancer, members):
        """
        Detaches a list of members from a balancer (the API supports up to 10).
        This method returns immediately.

        :param balancer: The Balancer to detach members from.
        :type  balancer: :class:`LoadBalancer`

        :param members: A list of Members to detach.
        :type  members: ``list`` of :class:`Member`

        :return: Returns whether the detach request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/nodes' % (balancer.id)
        ids = [('id', member.id) for member in members]
        resp = self.connection.request(uri, method='DELETE', params=ids)

        return resp.status == httplib.ACCEPTED

    def balancer_list_members(self, balancer):
        uri = '/loadbalancers/%s/nodes' % (balancer.id)
        data = self.connection.request(uri).object
        return self._to_members(data, balancer)

    def update_balancer(self, balancer, **kwargs):
        attrs = self._kwargs_to_mutable_attrs(**kwargs)
        resp = self.connection.async_request(
            action='/loadbalancers/%s' % balancer.id,
            method='PUT',
            data=json.dumps(attrs))
        return self._to_balancer(resp.object["loadBalancer"])

    def ex_update_balancer_no_poll(self, balancer, **kwargs):
        """
        Update balancer no poll.

        @inherits: :class:`Driver.update_balancer`
        """
        attrs = self._kwargs_to_mutable_attrs(**kwargs)
        resp = self.connection.request(
            action='/loadbalancers/%s' % balancer.id,
            method='PUT',
            data=json.dumps(attrs))
        return resp.status == httplib.ACCEPTED

    def ex_balancer_update_member(self, balancer, member, **kwargs):
        """
        Updates a Member's extra attributes for a Balancer.  The attributes can
        include 'weight' or 'condition'.  This method blocks until the update
        request has been processed and the balancer is in a RUNNING state
        again.

        :param balancer: Balancer to update the member on.
        :type  balancer: :class:`LoadBalancer`

        :param member: Member which should be used
        :type member: :class:`Member`

        :keyword **kwargs: New attributes.  Should contain either 'weight'
        or 'condition'.  'condition' can be set to 'ENABLED', 'DISABLED'.
        or 'DRAINING'.  'weight' can be set to a positive integer between
        1 and 100, with a higher weight indicating that the node will receive
        more traffic (assuming the Balancer is using a weighted algorithm).
        :type **kwargs: ``dict``

        :return: Updated Member.
        :rtype: :class:`Member`
        """
        accepted = self.ex_balancer_update_member_no_poll(
            balancer, member, **kwargs)

        if not accepted:
            msg = 'Update member attributes was not accepted'
            raise LibcloudError(msg, driver=self)

        balancer = self._get_updated_balancer(balancer)
        members = balancer.extra['members']

        updated_members = [m for m in members if m.id == member.id]

        if not updated_members:
            raise LibcloudError('Could not find updated member')

        return updated_members[0]

    def ex_balancer_update_member_no_poll(self, balancer, member, **kwargs):
        """
        Updates a Member's extra attributes for a Balancer.  The attribute can
        include 'weight' or 'condition'.  This method returns immediately.

        :param balancer: Balancer to update the member on.
        :type balancer: :class:`LoadBalancer`

        :param member: Member which should be used
        :type member: :class:`Member`

        :keyword **kwargs: New attributes.  Should contain either 'weight'
        or 'condition'.  'condition' can be set to 'ENABLED', 'DISABLED'.
        or 'DRAINING'.  'weight' can be set to a positive integer between
        1 and 100, with a higher weight indicating that the node will receive
        more traffic (assuming the Balancer is using a weighted algorithm).
        :type **kwargs: ``dict``

        :return: Returns whether the update request was accepted.
        :rtype: ``bool``
        """
        resp = self.connection.request(
            action='/loadbalancers/%s/nodes/%s' % (balancer.id, member.id),
            method='PUT',
            data=json.dumps(self._kwargs_to_mutable_member_attrs(**kwargs))
        )

        return resp.status == httplib.ACCEPTED

    def ex_list_algorithm_names(self):
        """
        Lists algorithms supported by the API.  Returned as strings because
        this list may change in the future.

        :rtype: ``list`` of ``str``
        """
        response = self.connection.request('/loadbalancers/algorithms')
        return [a["name"].upper() for a in response.object["algorithms"]]

    def ex_get_balancer_error_page(self, balancer):
        """
        List error page configured for the specified load balancer.

        :param balancer: Balancer which should be used
        :type balancer: :class:`LoadBalancer`

        :rtype: ``str``
        """
        uri = '/loadbalancers/%s/errorpage' % (balancer.id)
        resp = self.connection.request(uri)

        return resp.object["errorpage"]["content"]

    def ex_balancer_access_list(self, balancer):
        """
        List the access list.

        :param balancer: Balancer which should be used
        :type balancer: :class:`LoadBalancer`

        :rtype: ``list`` of :class:`RackspaceAccessRule`
        """
        uri = '/loadbalancers/%s/accesslist' % (balancer.id)
        resp = self.connection.request(uri)

        return [self._to_access_rule(el) for el in resp.object["accessList"]]

    def _get_updated_balancer(self, balancer):
        """
        Updating a balancer's attributes puts a balancer into
        'PENDING_UPDATE' status.  Wait until the balancer is
        back in 'ACTIVE' status and then return the individual
        balancer details call.
        """
        resp = self.connection.async_request(
            action='/loadbalancers/%s' % balancer.id,
            method='GET')

        return self._to_balancer(resp.object['loadBalancer'])

    def ex_update_balancer_health_monitor(self, balancer, health_monitor):
        """
        Sets a Balancer's health monitor.  This method blocks until the update
        request has been processed and the balancer is in a RUNNING state
        again.

        :param balancer: Balancer to update.
        :type  balancer: :class:`LoadBalancer`

        :param health_monitor: Health Monitor for the balancer.
        :type  health_monitor: :class:`RackspaceHealthMonitor`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        accepted = self.ex_update_balancer_health_monitor_no_poll(
            balancer, health_monitor)
        if not accepted:
            msg = 'Update health monitor request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_update_balancer_health_monitor_no_poll(self, balancer,
                                                  health_monitor):
        """
        Sets a Balancer's health monitor.  This method returns immediately.

        :param balancer: Balancer to update health monitor on.
        :type  balancer: :class:`LoadBalancer`

        :param health_monitor: Health Monitor for the balancer.
        :type  health_monitor: :class:`RackspaceHealthMonitor`

        :return: Returns whether the update request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/healthmonitor' % (balancer.id)

        resp = self.connection.request(
            uri, method='PUT', data=json.dumps(health_monitor._to_dict()))

        return resp.status == httplib.ACCEPTED

    def ex_disable_balancer_health_monitor(self, balancer):
        """
        Disables a Balancer's health monitor.  This method blocks until the
        disable request has been processed and the balancer is in a RUNNING
        state again.

        :param balancer: Balancer to disable health monitor on.
        :type  balancer: :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_disable_balancer_health_monitor_no_poll(balancer):
            msg = 'Disable health monitor request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_disable_balancer_health_monitor_no_poll(self, balancer):
        """
        Disables a Balancer's health monitor.  This method returns
        immediately.

        :param balancer: Balancer to disable health monitor on.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the disable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/healthmonitor' % (balancer.id)

        resp = self.connection.request(uri,
                                       method='DELETE')

        return resp.status == httplib.ACCEPTED

    def ex_update_balancer_connection_throttle(self, balancer,
                                               connection_throttle):
        """
        Updates a Balancer's connection throttle.  This method blocks until
        the update request has been processed and the balancer is in a
        RUNNING state again.

        :param balancer: Balancer to update connection throttle on.
        :type  balancer: :class:`LoadBalancer`

        :param connection_throttle: Connection Throttle for the balancer.
        :type  connection_throttle: :class:`RackspaceConnectionThrottle`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        accepted = self.ex_update_balancer_connection_throttle_no_poll(
            balancer, connection_throttle)

        if not accepted:
            msg = 'Update connection throttle request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_update_balancer_connection_throttle_no_poll(self, balancer,
                                                       connection_throttle):
        """
        Sets a Balancer's connection throttle.  This method returns
        immediately.

        :param balancer: Balancer to update connection throttle on.
        :type  balancer: :class:`LoadBalancer`

        :param connection_throttle: Connection Throttle for the balancer.
        :type  connection_throttle: :class:`RackspaceConnectionThrottle`

        :return: Returns whether the update request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/connectionthrottle' % (balancer.id)
        resp = self.connection.request(
            uri, method='PUT',
            data=json.dumps(connection_throttle._to_dict()))

        return resp.status == httplib.ACCEPTED

    def ex_disable_balancer_connection_throttle(self, balancer):
        """
        Disables a Balancer's connection throttle.  This method blocks until
        the disable request has been processed and the balancer is in a RUNNING
        state again.

        :param balancer: Balancer to disable connection throttle on.
        :type  balancer: :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_disable_balancer_connection_throttle_no_poll(balancer):
            msg = 'Disable connection throttle request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_disable_balancer_connection_throttle_no_poll(self, balancer):
        """
        Disables a Balancer's connection throttle.  This method returns
        immediately.

        :param balancer: Balancer to disable connection throttle on.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the disable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/connectionthrottle' % (balancer.id)
        resp = self.connection.request(uri, method='DELETE')

        return resp.status == httplib.ACCEPTED

    def ex_enable_balancer_connection_logging(self, balancer):
        """
        Enables connection logging for a Balancer.  This method blocks until
        the enable request has been processed and the balancer is in a RUNNING
        state again.

        :param balancer: Balancer to enable connection logging on.
        :type  balancer: :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_enable_balancer_connection_logging_no_poll(balancer):
            msg = 'Enable connection logging request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_enable_balancer_connection_logging_no_poll(self, balancer):
        """
        Enables connection logging for a Balancer.  This method returns
        immediately.

        :param balancer: Balancer to enable connection logging on.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the enable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/connectionlogging' % (balancer.id)

        resp = self.connection.request(
            uri, method='PUT',
            data=json.dumps({'connectionLogging': {'enabled': True}})
        )

        return resp.status == httplib.ACCEPTED

    def ex_disable_balancer_connection_logging(self, balancer):
        """
        Disables connection logging for a Balancer.  This method blocks until
        the enable request has been processed and the balancer is in a RUNNING
        state again.

        :param balancer: Balancer to disable connection logging on.
        :type  balancer: :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_disable_balancer_connection_logging_no_poll(balancer):
            msg = 'Disable connection logging request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_disable_balancer_connection_logging_no_poll(self, balancer):
        """
        Disables connection logging for a Balancer.  This method returns
        immediately.

        :param balancer: Balancer to disable connection logging on.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the disable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/connectionlogging' % (balancer.id)
        resp = self.connection.request(
            uri, method='PUT',
            data=json.dumps({'connectionLogging': {'enabled': False}})
        )

        return resp.status == httplib.ACCEPTED

    def ex_enable_balancer_session_persistence(self, balancer):
        """
        Enables session persistence for a Balancer by setting the persistence
        type to 'HTTP_COOKIE'.  This method blocks until the enable request
        has been processed and the balancer is in a RUNNING state again.

        :param balancer: Balancer to enable session persistence on.
        :type  balancer: :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_enable_balancer_session_persistence_no_poll(balancer):
            msg = 'Enable session persistence request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_enable_balancer_session_persistence_no_poll(self, balancer):
        """
        Enables session persistence for a Balancer by setting the persistence
        type to 'HTTP_COOKIE'.  This method returns immediately.

        :param balancer: Balancer to enable session persistence on.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the enable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/sessionpersistence' % (balancer.id)
        resp = self.connection.request(
            uri, method='PUT',
            data=json.dumps(
                {'sessionPersistence': {'persistenceType': 'HTTP_COOKIE'}})
        )

        return resp.status == httplib.ACCEPTED

    def ex_disable_balancer_session_persistence(self, balancer):
        """
        Disables session persistence for a Balancer.  This method blocks until
        the disable request has been processed and the balancer is in a RUNNING
        state again.

        :param balancer: Balancer to disable session persistence on.
        :type balancer:  :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_disable_balancer_session_persistence_no_poll(balancer):
            msg = 'Disable session persistence request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_disable_balancer_session_persistence_no_poll(self, balancer):
        """
        Disables session persistence for a Balancer.  This method returns
        immediately.

        :param balancer: Balancer to disable session persistence for.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the disable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/sessionpersistence' % (balancer.id)
        resp = self.connection.request(uri, method='DELETE')

        return resp.status == httplib.ACCEPTED

    def ex_update_balancer_error_page(self, balancer, page_content):
        """
        Updates a Balancer's custom error page.  This method blocks until
        the update request has been processed and the balancer is in a
        RUNNING state again.

        :param balancer: Balancer to update the custom error page for.
        :type  balancer: :class:`LoadBalancer`

        :param page_content: HTML content for the custom error page.
        :type  page_content: ``str``

        :return: Updated Balancer.
        :rtype:  :class:`LoadBalancer`
        """
        accepted = self.ex_update_balancer_error_page_no_poll(balancer,
                                                              page_content)
        if not accepted:
            msg = 'Update error page request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_update_balancer_error_page_no_poll(self, balancer, page_content):
        """
        Updates a Balancer's custom error page.  This method returns
        immediately.

        :param balancer: Balancer to update the custom error page for.
        :type  balancer: :class:`LoadBalancer`

        :param page_content: HTML content for the custom error page.
        :type  page_content: ``str``

        :return: Returns whether the update request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/errorpage' % (balancer.id)
        resp = self.connection.request(
            uri, method='PUT',
            data=json.dumps({'errorpage': {'content': page_content}})
        )

        return resp.status == httplib.ACCEPTED

    def ex_disable_balancer_custom_error_page(self, balancer):
        """
        Disables a Balancer's custom error page, returning its error page to
        the Rackspace-provided default.  This method blocks until the disable
        request has been processed and the balancer is in a RUNNING state
        again.

        :param balancer: Balancer to disable the custom error page for.
        :type  balancer: :class:`LoadBalancer`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        if not self.ex_disable_balancer_custom_error_page_no_poll(balancer):
            msg = 'Disable custom error page request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_disable_balancer_custom_error_page_no_poll(self, balancer):
        """
        Disables a Balancer's custom error page, returning its error page to
        the Rackspace-provided default.  This method returns immediately.

        :param balancer: Balancer to disable the custom error page for.
        :type  balancer: :class:`LoadBalancer`

        :return: Returns whether the disable request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/errorpage' % (balancer.id)
        resp = self.connection.request(uri, method='DELETE')

        # Load Balancer API currently returns 200 OK on custom error page
        # delete.
        return resp.status == httplib.OK or resp.status == httplib.ACCEPTED

    def ex_create_balancer_access_rule(self, balancer, rule):
        """
        Adds an access rule to a Balancer's access list.  This method blocks
        until the update request has been processed and the balancer is in a
        RUNNING state again.

        :param balancer: Balancer to create the access rule for.
        :type balancer: :class:`LoadBalancer`

        :param rule: Access Rule to add to the balancer.
        :type rule: :class:`RackspaceAccessRule`

        :return: The created access rule.
        :rtype: :class:`RackspaceAccessRule`
        """
        accepted = self.ex_create_balancer_access_rule_no_poll(balancer, rule)
        if not accepted:
            msg = 'Create access rule not accepted'
            raise LibcloudError(msg, driver=self)

        balancer = self._get_updated_balancer(balancer)
        access_list = balancer.extra['accessList']

        created_rule = self._find_matching_rule(rule, access_list)

        if not created_rule:
            raise LibcloudError('Could not find created rule')

        return created_rule

    def ex_create_balancer_access_rule_no_poll(self, balancer, rule):
        """
        Adds an access rule to a Balancer's access list.  This method returns
        immediately.

        :param balancer: Balancer to create the access rule for.
        :type balancer: :class:`LoadBalancer`

        :param rule: Access Rule to add to the balancer.
        :type rule: :class:`RackspaceAccessRule`

        :return: Returns whether the create request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/accesslist' % (balancer.id)
        resp = self.connection.request(
            uri, method='POST',
            data=json.dumps({'networkItem': rule._to_dict()})
        )

        return resp.status == httplib.ACCEPTED

    def ex_create_balancer_access_rules(self, balancer, rules):
        """
        Adds a list of access rules to a Balancer's access list.  This method
        blocks until the update request has been processed and the balancer is
        in a RUNNING state again.

        :param balancer: Balancer to create the access rule for.
        :type  balancer: :class:`LoadBalancer`

        :param rules: List of :class:`RackspaceAccessRule` to add to the
                      balancer.
        :type  rules: ``list`` of :class:`RackspaceAccessRule`

        :return: The created access rules.
        :rtype: :class:`RackspaceAccessRule`
        """
        accepted = self.ex_create_balancer_access_rules_no_poll(balancer,
                                                                rules)
        if not accepted:
            msg = 'Create access rules not accepted'
            raise LibcloudError(msg, driver=self)

        balancer = self._get_updated_balancer(balancer)
        access_list = balancer.extra['accessList']

        created_rules = []
        for r in rules:
            matched_rule = self._find_matching_rule(r, access_list)
            if matched_rule:
                created_rules.append(matched_rule)

        if len(created_rules) != len(rules):
            raise LibcloudError('Could not find all created rules')

        return created_rules

    def _find_matching_rule(self, rule_to_find, access_list):
        """
        LB API does not return the ID for the newly created rules, so we have
        to search the list to find the rule with a matching rule type and
        address to return an object with the right identifier.it.  The API
        enforces rule type and address uniqueness.
        """
        for r in access_list:
            if rule_to_find.rule_type == r.rule_type and\
                    rule_to_find.address == r.address:
                return r

        return None

    def ex_create_balancer_access_rules_no_poll(self, balancer, rules):
        """
        Adds a list of access rules to a Balancer's access list.  This method
        returns immediately.

        :param balancer: Balancer to create the access rule for.
        :type balancer: :class:`LoadBalancer`

        :param rules: List of :class:`RackspaceAccessRule` to add to
                      the balancer.
        :type  rules: ``list`` of :class:`RackspaceAccessRule`

        :return: Returns whether the create request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/accesslist' % (balancer.id)
        resp = self.connection.request(
            uri, method='POST',
            data=json.dumps({'accessList':
                             [rule._to_dict() for rule in rules]})
        )

        return resp.status == httplib.ACCEPTED

    def ex_destroy_balancer_access_rule(self, balancer, rule):
        """
        Removes an access rule from a Balancer's access list.  This method
        blocks until the update request has been processed and the balancer
        is in a RUNNING state again.

        :param balancer: Balancer to remove the access rule from.
        :type  balancer: :class:`LoadBalancer`

        :param rule: Access Rule to remove from the balancer.
        :type  rule: :class:`RackspaceAccessRule`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        accepted = self.ex_destroy_balancer_access_rule_no_poll(balancer, rule)
        if not accepted:
            msg = 'Delete access rule not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_destroy_balancer_access_rule_no_poll(self, balancer, rule):
        """
        Removes an access rule from a Balancer's access list.  This method
        returns immediately.

        :param balancer: Balancer to remove the access rule from.
        :type  balancer: :class:`LoadBalancer`

        :param rule: Access Rule to remove from the balancer.
        :type  rule: :class:`RackspaceAccessRule`

        :return: Returns whether the destroy request was accepted.
        :rtype: ``bool``
        """
        uri = '/loadbalancers/%s/accesslist/%s' % (balancer.id, rule.id)
        resp = self.connection.request(uri, method='DELETE')

        return resp.status == httplib.ACCEPTED

    def ex_destroy_balancer_access_rules(self, balancer, rules):
        """
        Removes a list of access rules from a Balancer's access list.  This
        method blocks until the update request has been processed and the
        balancer is in a RUNNING state again.

        :param balancer: Balancer to remove the access rules from.
        :type  balancer: :class:`LoadBalancer`

        :param rules: List of :class:`RackspaceAccessRule` objects to remove
                      from the balancer.
        :type  rules: ``list`` of :class:`RackspaceAccessRule`

        :return: Updated Balancer.
        :rtype: :class:`LoadBalancer`
        """
        accepted = self.ex_destroy_balancer_access_rules_no_poll(
            balancer, rules)

        if not accepted:
            msg = 'Destroy access rules request not accepted'
            raise LibcloudError(msg, driver=self)

        return self._get_updated_balancer(balancer)

    def ex_destroy_balancer_access_rules_no_poll(self, balancer, rules):
        """
        Removes a list of access rules from a Balancer's access list.  This
        method returns immediately.

        :param balancer: Balancer to remove the access rules from.
        :type  balancer: :class:`LoadBalancer`

        :param rules: List of :class:`RackspaceAccessRule` objects to remove
                      from the balancer.
        :type  rules: ``list`` of :class:`RackspaceAccessRule`

        :return: Returns whether the destroy request was accepted.
        :rtype: ``bool``
        """
        ids = [('id', rule.id) for rule in rules]
        uri = '/loadbalancers/%s/accesslist' % balancer.id

        resp = self.connection.request(uri,
                                       method='DELETE',
                                       params=ids)

        return resp.status == httplib.ACCEPTED

    def ex_list_current_usage(self, balancer):
        """
        Return current load balancer usage report.

        :param balancer: Balancer to remove the access rules from.
        :type  balancer: :class:`LoadBalancer`

        :return: Raw load balancer usage object.
        :rtype: ``dict``
        """
        uri = '/loadbalancers/%s/usage/current' % (balancer.id)
        resp = self.connection.request(uri, method='GET')

        return resp.object

    def _to_protocols(self, object):
        protocols = []
        for item in object["protocols"]:
            protocols.append(item['name'].lower())
        return protocols

    def _to_protocols_with_default_ports(self, object):
        protocols = []
        for item in object["protocols"]:
            name = item['name'].lower()
            port = int(item['port'])
            protocols.append((name, port))

        return protocols

    def _to_balancers(self, object):
        return [self._to_balancer(el) for el in object["loadBalancers"]]

    def _to_balancer(self, el):
        ip = None
        port = None
        sourceAddresses = {}

        if 'port' in el:
            port = el["port"]

        if 'sourceAddresses' in el:
            sourceAddresses = el['sourceAddresses']

        extra = {
            "ipv6PublicSource": sourceAddresses.get("ipv6Public"),
            "ipv4PublicSource": sourceAddresses.get("ipv4Public"),
            "ipv4PrivateSource": sourceAddresses.get("ipv4Servicenet"),
            "service_name": self.connection.get_service_name(),
            "uri": "https://%s%s/loadbalancers/%s" % (
                self.connection.host, self.connection.request_path, el["id"]),
        }

        if 'virtualIps' in el:
            ip = el['virtualIps'][0]['address']
            extra['virtualIps'] = el['virtualIps']

        if 'protocol' in el:
            extra['protocol'] = el['protocol']

        if 'algorithm' in el and \
           el["algorithm"] in self._VALUE_TO_ALGORITHM_MAP:
            extra["algorithm"] = self._value_to_algorithm(el["algorithm"])

        if 'healthMonitor' in el:
            health_monitor = self._to_health_monitor(el)
            if health_monitor:
                extra["healthMonitor"] = health_monitor

        if 'connectionThrottle' in el:
            extra["connectionThrottle"] = self._to_connection_throttle(el)

        if 'sessionPersistence' in el:
            persistence = el["sessionPersistence"]
            extra["sessionPersistenceType"] =\
                persistence.get("persistenceType")

        if 'connectionLogging' in el:
            logging = el["connectionLogging"]
            extra["connectionLoggingEnabled"] = logging.get("enabled")

        if 'nodes' in el:
            extra['members'] = self._to_members(el)

        if 'created' in el:
            extra['created'] = self._iso_to_datetime(el['created']['time'])

        if 'updated' in el:
            extra['updated'] = self._iso_to_datetime(el['updated']['time'])

        if 'accessList' in el:
            extra['accessList'] = [self._to_access_rule(rule)
                                   for rule in el['accessList']]

        return LoadBalancer(id=el["id"],
                            name=el["name"],
                            state=self.LB_STATE_MAP.get(
                                el["status"], State.UNKNOWN),
                            ip=ip,
                            port=port,
                            driver=self.connection.driver,
                            extra=extra)

    def _to_members(self, object, balancer=None):
        return [self._to_member(el, balancer) for el in object["nodes"]]

    def _to_member(self, el, balancer=None):
        extra = {}
        if 'weight' in el:
            extra['weight'] = el["weight"]

        if 'condition' in el and\
           el['condition'] in self.LB_MEMBER_CONDITION_MAP:
            extra['condition'] =\
                self.LB_MEMBER_CONDITION_MAP.get(el["condition"])

        if 'status' in el:
            extra['status'] = el["status"]

        lbmember = Member(id=el["id"],
                          ip=el["address"],
                          port=el["port"],
                          balancer=balancer,
                          extra=extra)
        return lbmember

    def _protocol_to_value(self, protocol):
        non_standard_protocols = {'imapv2': 'IMAPv2', 'imapv3': 'IMAPv3',
                                  'imapv4': 'IMAPv4'}
        protocol_name = protocol.lower()

        if protocol_name in non_standard_protocols:
            protocol_value = non_standard_protocols[protocol_name]
        else:
            protocol_value = protocol.upper()

        return protocol_value

    def _kwargs_to_mutable_attrs(self, **attrs):
        update_attrs = {}
        if "name" in attrs:
            update_attrs['name'] = attrs['name']

        if "algorithm" in attrs:
            algorithm_value = self._algorithm_to_value(attrs['algorithm'])
            update_attrs['algorithm'] = algorithm_value

        if "protocol" in attrs:
            update_attrs['protocol'] =\
                self._protocol_to_value(attrs['protocol'])

        if "port" in attrs:
            update_attrs['port'] = int(attrs['port'])

        if "vip" in attrs:
            if attrs['vip'] == 'PUBLIC' or attrs['vip'] == 'SERVICENET':
                update_attrs['virtualIps'] = [{'type': attrs['vip']}]
            else:
                update_attrs['virtualIps'] = [{'id': attrs['vip']}]

        return update_attrs

    def _kwargs_to_mutable_member_attrs(self, **attrs):
        update_attrs = {}
        if 'condition' in attrs:
            update_attrs['condition'] =\
                self.CONDITION_LB_MEMBER_MAP.get(attrs['condition'])

        if 'weight' in attrs:
            update_attrs['weight'] = attrs['weight']

        return update_attrs

    def _to_health_monitor(self, el):
        health_monitor_data = el["healthMonitor"]

        type = health_monitor_data.get("type")
        delay = health_monitor_data.get("delay")
        timeout = health_monitor_data.get("timeout")
        attempts_before_deactivation =\
            health_monitor_data.get("attemptsBeforeDeactivation")

        if type == "CONNECT":
            return RackspaceHealthMonitor(
                type=type, delay=delay, timeout=timeout,
                attempts_before_deactivation=attempts_before_deactivation)

        if type == "HTTP" or type == "HTTPS":
            return RackspaceHTTPHealthMonitor(
                type=type, delay=delay, timeout=timeout,
                attempts_before_deactivation=attempts_before_deactivation,
                path=health_monitor_data.get("path"),
                status_regex=health_monitor_data.get("statusRegex"),
                body_regex=health_monitor_data.get("bodyRegex", ''))

        return None

    def _to_connection_throttle(self, el):
        connection_throttle_data = el["connectionThrottle"]

        min_connections = connection_throttle_data.get("minConnections")
        max_connections = connection_throttle_data.get("maxConnections")
        max_connection_rate = connection_throttle_data.get("maxConnectionRate")
        rate_interval = connection_throttle_data.get("rateInterval")

        return RackspaceConnectionThrottle(
            min_connections=min_connections,
            max_connections=max_connections,
            max_connection_rate=max_connection_rate,
            rate_interval_seconds=rate_interval)

    def _to_access_rule(self, el):
        return RackspaceAccessRule(
            id=el.get("id"),
            rule_type=self._to_access_rule_type(el.get("type")),
            address=el.get("address"))

    def _to_access_rule_type(self, type):
        if type == "ALLOW":
            return RackspaceAccessRuleType.ALLOW
        elif type == "DENY":
            return RackspaceAccessRuleType.DENY

    def _iso_to_datetime(self, isodate):
        date_formats = ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S%z')
        date = None

        for date_format in date_formats:
            try:
                date = datetime.strptime(isodate, date_format)
            except ValueError:
                pass

            if date:
                break

        return date
