# 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.
"""
1&1 Cloud Server Compute driver
"""
import json

from libcloud.compute.providers import Provider
from libcloud.common.base import JsonResponse, ConnectionKey
from libcloud.compute.base import NodeSize, NodeImage, NodeLocation, \
    Node, NodeAuthPassword, NodeAuthSSHKey
from libcloud.common.types import InvalidCredsError
from libcloud.compute.types import NodeState
from libcloud.utils.py3 import httplib
from libcloud.compute.base import NodeDriver

from time import sleep

API_HOST = 'cloudpanel-api.1and1.com'
API_VERSION = '/v1/'

__all__ = [
    'API_HOST',
    'API_VERSION',
    'OneAndOneResponse',
    'OneAndOneConnection',
    'OneAndOneNodeDriver'
]


class OneAndOneResponse(JsonResponse):
    """
    OneAndOne response parsing.
    """
    valid_response_codes = [httplib.OK, httplib.CREATED, httplib.ACCEPTED]

    def parse_error(self):

        if self.status == httplib.UNAUTHORIZED:
            body = self.parse_body()
            raise InvalidCredsError(body['message'])
        else:
            body = self.parse_body()
            if 'message' in body:
                error = '%s (code: %s)' % (body['message'], self.status)
            else:
                error = body
            return error

    def success(self):
        return self.status in self.valid_response_codes


class OneAndOneConnection(ConnectionKey):
    """
    Connection class for the 1&1 driver
    """

    host = API_HOST
    api_prefix = API_VERSION
    responseCls = OneAndOneResponse

    def encode_data(self, data):
        return json.dumps(data)

    def add_default_headers(self, headers):
        """
        Add headers that are necessary for every request

        This method adds ``token`` and ``Content-Type`` to the request.
        """
        headers['X-Token'] = self.key
        headers['Content-Type'] = 'application/json'
        return headers

    def request(self, action, params=None, data=None, headers=None,
                method='GET', raw=False):
        """
        Some requests will use the href attribute directly.
        If this is not the case, then we should formulate the
        url based on the action specified.
        If we are using a full url, we need to remove the
        host and protocol components.
        """
        action = self.api_prefix + action.lstrip('/')

        return super(OneAndOneConnection, self). \
            request(action=action,
                    params=params,
                    data=data,
                    headers=headers,
                    method=method,
                    raw=raw)


class OneAndOneNodeDriver(NodeDriver):
    """
      Base OneAndOne node driver.
    """
    connectionCls = OneAndOneConnection
    name = '1and1'
    website = 'http://www.1and1.com'
    type = Provider.ONEANDONE

    NODE_STATE_MAP = {
        'POWERING_ON': NodeState.STARTING,
        'POWERING_OFF': NodeState.PENDING,
        'POWERED_OFF': NodeState.STOPPING,
        'POWERED_ON': NodeState.RUNNING,
        'REBOOTING': NodeState.REBOOTING,
        'CONFIGURING': NodeState.RECONFIGURING,
        'REMOVING': NodeState.UNKNOWN,
        'DEPLOYING': NodeState.STARTING,
    }

    """
    Core Functions
    """

    def list_sizes(self):
        """
        Lists all sizes

        :return: A list of all configurable node sizes.
        :rtype: ``list`` of :class:`NodeSize`
        """
        sizes = []

        fixed_instances = self._list_fixed_instances()
        for value in fixed_instances:
            node_size = self._to_node_size(value)
            sizes.append(node_size)

        return sizes

    def list_locations(self):
        """
        Lists all locations

        :return: ``list`` of :class:`NodeLocation`
        :rtype: ``list``
        """
        datacenters = self.ex_list_datacenters()
        locations = []
        for values in datacenters:
            node_size = self._to_location(values)
            locations.append(node_size)

        return locations

    def list_images(self, image_type=None):
        """
        :return: ``list`` of :class: `NodeImage`
        :rtype: ``list``
        """
        response = self.connection.request(
            action='server_appliances',
            method='GET'
        )

        return self._to_images(response.object, image_type)

    def get_image(self, image_id):
        response = self.connection.request(
            action='server_appliances/%s' % image_id,
            method='GET'
        )
        return self._to_image(response.object)

    """
    Node functions
    """

    def create_node(self,
                    name,
                    image,
                    ex_fixed_instance_size_id,
                    location=None,
                    auth=None,
                    ex_ip=None,
                    ex_monitoring_policy_id=None,
                    ex_firewall_policy_id=None,
                    ex_loadbalancer_id=None,
                    ex_description=None,
                    ex_power_on=None):
        """
        Creates a node.

        :param name: The name of the new node
        :type name: `str`

        :param ex_fixed_instance_size_id:
        Fixed instance size ID from list_sizes
        :type ex_fixed_instance_size_id: ``str``

        :param location: 1&1 Data center Location
        :type location: `NodeLocation`

        :param ex_ip: IP address
        :type ex_ip: `str`

        :param ex_ssh_key: SSH Key
        :type ex_ssh_key: `str`

        :param password: Password
        :type password: `str`

        :param ex_monitoring_policy_id:
        :type ex_firewall_policy_id: `str`

        :param ex_firewall_policy_id:
        :type ex_firewall_policy_id: `str`

        :param ex_loadbalancer_id:
        :type ex_loadbalancer_id: `str`

        :param ex_description:
        :type ex_description: `str`

        :param ex_power_on:
        :type ex_power_on: `bool`

        :return:    Instance of class ``Node``
        :rtype:     :class:`Node`
        """

        body = {
            'name': name,
            'appliance_id': image.id,
            'hardware': {
                'fixed_instance_size_id': ex_fixed_instance_size_id
            },
        }

        if location is not None:
            body['datacenter_id'] = location.id
        if ex_power_on is not None:
            body['power_on'] = ex_power_on

        if ex_description is not None:
            body['description'] = ex_description

        if ex_firewall_policy_id is not None:
            body['firewall_policy_id'] = ex_firewall_policy_id

        if ex_monitoring_policy_id is not None:
            body['monitoring_policy_id'] = ex_monitoring_policy_id

        if ex_loadbalancer_id is not None:
            body['loadbalancer_id'] = ex_loadbalancer_id

        if auth is not None:
            if isinstance(auth, NodeAuthPassword):
                body['password'] = auth.password
            elif isinstance(auth, NodeAuthSSHKey):
                body['rsa_key'] = auth.pubkey
        if ex_ip is not None:
            body['ip_id'] = ex_ip

        response = self.connection.request(
            action='servers',
            data=body,
            method='POST',
        )

        return self._to_node(response.object)

    def list_nodes(self):
        """
        List all nodes.

        :return: ``list`` of :class:`Node`
        :rtype: ``list``
        """
        response = self.connection.request(
            action='servers',
            method='GET'
        )

        return self._to_nodes(response.object)

    def destroy_node(self, node, ex_keep_ips=False):
        """
        Destroys a node.

        :param node: The node you wish to destroy.
        :type volume: :class:`Node`

        :param ex_keep_ips: True to keep all IP addresses assigned to the node
        :type ex_keep_ips: : ``bool``

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """
        self.ex_shutdown_server(node.id)

        self._wait_for_state(node.id, 'POWERED_OFF')

        response = self.connection.request(
            action='servers/%s' % node.id,
            params={'keep_ips': ex_keep_ips},
            method='DELETE'
        )

        return self._to_node(response.object)

    def reboot_node(self, node):
        """
        Reboots the node.

        :param node: The node you wish to destroy.
        :type volume: :class:`Node`

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """
        shutdown_body = {
            "action": "REBOOT",
            "method": "HARDWARE"
        }
        response = self.connection.request(
            action='servers/%s/status/action' % node.id,
            data=shutdown_body,
            method='PUT',
        )
        return self._to_node(response.object)

    """
    Extension functions
    """

    def ex_rename_server(self, server_id, name=None, description=None):
        """
        Renames the server
        :param  server_id: ID of the server you want to rename

        :param  name: New name of the server
        :type: ``str``

        :param description: New description of the server
        :type: ``str``

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """

        body = {}
        if name is not None:
            body["name"] = name
        if description is not None:
            body["description"] = description

        response = self.connection.request(
            action='servers/%s' % server_id,
            data=body,
            method='PUT'
        )

        return self._to_node(response.object)

    def ex_get_server_hardware(self, server_id):
        """
        Gets all server hardware

        :param server_id: Id of the server
        :type: ``str``

        :return: Server's hardware
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='servers/%s/hardware' % server_id,
            method='GET'
        )
        return response.object

    """
    Hardware operations
    """

    def ex_modify_server_hardware(self, server_id,
                                  fixed_instance_size_id=None, vcore=None,
                                  cores_per_processor=None, ram=None):
        """
        Modifies server's hardware

        :param server_id:
        :type: ``str``

        :param fixed_instance_size_id: Id of the fixed instance size
        :type: ``str``

        :param vcore: Virtual cores count
        :type: ``int``

        :param cores_per_processor: Count of cores per procesor
        :type: ``int``

        :param ram: Amount of ram for the server
        :type: ``int``

        :return:    Instance of class ``Node``
        :type:     :class: `Node`
        """

        body = {}

        if fixed_instance_size_id is not None:
            body['firewall_policy_id'] = fixed_instance_size_id
        if vcore is not None:
            body['vcore'] = vcore
        if cores_per_processor is not None:
            body['cores_per_processor'] = cores_per_processor
        if ram is not None:
            body['ram'] = ram

        response = self.connection.request(
            action='servers/%s/hardware' % server_id,
            data=body,
            method='PUT'
        )

        return self._to_node(response.object)

    """
    HDD operations
    """

    def ex_modify_server_hdd(self, server_id, hdd_id=None, size=None):
        """
        Modifies server hard disk drives

        :param server_id: Id of the server
        :type: ``str``

        :param hdd_id: Id of the hard disk
        :type: ``str``

        :param size: Size of the hard disk
        :type: ``str``

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """

        body = {}

        if size is not None:
            body['size'] = size

            response = self.connection.request(
                action='servers/%s/hardware/hdds/%s' % (server_id, hdd_id),
                data=body,
                method='PUT'
            )

            return self._to_node(response.object)

    def ex_add_hdd(self, server_id, size, is_main):
        """
        Add a hard disk to the server

        :param server_id: Id of the server
        :type: ``str``

        :param size: Size of the new disk
        :type: ``str``

        :param is_main: Indicates if the disk is going to be the boot disk
        :type: ``boolean``

        :return:    Instance of class ``Node``
        :type:     :class: `Node`
        """
        body = {
            'size': size,
            'is_main': is_main
        }

        response = self.connection.request(
            action='servers/%s/hardware/hdds' % server_id,
            data=body,
            method='POST'
        )

        return self._to_node(response.object)

    def ex_remove_hdd(self, server_id, hdd_id):
        """
        Removes existing hard disk

        :param server_id: Id of the server
        :type: ``str``

        :param hdd_id: Id of the hard disk
        :type: ``str``

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """

        response = self.connection.request(
            action='servers/%s/hardware/hdds/%s' % (server_id, hdd_id),
            method='DELETE'
        )

        return self._to_node(response.object)

    """
    Data center operations
    """

    def ex_list_datacenters(self):
        """
        Lists all data centers

        :return: List of data centers
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='datacenters',
            method='GET'
        )

        return response.object

    def ex_get_server(self, server_id):
        """
        Gets a server

        :param server_id: Id of the server to be retrieved
        :type: ``str``

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """

        response = self.connection.request(
            action='servers/%s' % (server_id),
            method='GET'
        )

        return self._to_node(response.object)

    def ex_shutdown_server(self, server_id, method='SOFTWARE'):
        """
        Shuts down the server

        :param server_id: Id of the server to be shut down
        :type: ``str``

        :param method: Method of shutting down "SOFTWARE" or "HARDWARE"

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """

        shutdown_body = {
            'action': 'POWER_OFF',
            'method': method
        }
        response = self.connection.request(
            action='servers/%s/status/action' % (server_id),
            data=shutdown_body,
            method='PUT',
        )
        return self._to_node(response.object)

    """
    Image operations
    """

    def ex_get_server_image(self, server_id):
        """
        Gets server image

        :param server_id: Id of the server
        :type: ``str``

        :return: Server image
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='servers/%s/image' % server_id,
            method='GET'
        )
        return response.object

    def ex_reinstall_server_image(self, server_id, image_id, password=None):
        """
        Installs a new image on the server

        :param server_id: Id of the server
        :type: ``str``

        :param image_id: Id of the image (Server Appliance)
        :type: ``str``

        :param password: New password for the server

        :return:    Instance of class ``Node``
        :rtype:     :class: `Node`
        """

        body = {
            'id': image_id,
        }

        if password is not None:
            body['password'] = password

        response = self.connection.request(
            action='servers/%s/image' % server_id,
            data=body,
            method='PUT'
        )
        return self._to_node(response.object)

    """
    Server IP operations
    """

    def ex_list_server_ips(self, server_id):
        """
        Gets all server IP objects

        :param server_id: Id of the server
        :type: ``str``

        :return: List of server IP objects
        :rtype: ``list`` of ``dict``
        """
        response = self.connection.request(
            action='servers/%s/ips' % server_id,
            method='GET'
        )

        return response.object

    def ex_get_server_ip(self, server_id, ip_id):
        """
        Get a single server IP object

        :param server_id: Id of the server
        :type: ``str``

        :param ip_id: ID of the IP address
        :type: ``str``

        :return: IP address object
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='servers/%s/ips/%s' % (server_id, ip_id),
            method='GET'
        )

        return response.object

    def ex_assign_server_ip(self, server_id, ip_type):
        """
        Assigns a new IP address to the server

        :param server_id: Id of the server
        :type: ``str``

        :param ip_type: Type of the IP address [IPV4,IPV6]
        :type: ``str``

        :return: ``Node`` instance
        :rtype: ``Node``
        """

        body = {
            'type': ip_type
        }

        response = self.connection.request(
            action='servers/%s/ips' % server_id,
            data=body,
            method='POST'
        )

        return self._to_node(response.object)

    def ex_remove_server_ip(self, server_id, ip_id, keep_ip=None):
        """
        Removes an IP address from the server

        :param server_id: Id of the server
        :type: ``str``

        :param ip_id: ID of the IP address
        :type: ``str``

        :param keep_ip: Indicates whether IP address will be removed from
                        the Cloud Panel
        :type: ``boolean``

        :return: ``Node`` instance
        :rtype: ``Node``
        """

        body = {}
        if keep_ip is not None:
            body['keep_ip'] = keep_ip

        response = self.connection.request(
            action='servers/%s/ips/%s' % (server_id, ip_id),
            data=body,
            method='DELETE'
        )

        return self._to_node(response.object)

    def ex_get_server_firewall_policies(self, server_id, ip_id):
        """
        Gets a firewall policy of attached to the server's IP

        :param server_id: Id of the server
        :type: ``str``

        :param ip_id: ID of the IP address
        :type: ``str``

        :return: IP address object
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='/servers/%s/ips/%s/firewall_policy' % (server_id, ip_id),
            method='GET'
        )

        return response.object

    def ex_remove_server_firewall_policy(self, server_id, ip_id):
        """
        Removes a firewall policy from server's IP

        :param server_id: Id of the server
        :type: ``str``

        :param ip_id: ID of the IP address
        :type: ``str``

        :return: ``Node`` instance
        :rtype: ``Node``
        """
        response = self.connection.request(
            action='/servers/%s/ips/%s/firewall_policy' % (server_id, ip_id),
            method='DELETE'
        )

        return self._to_node(response.object)

    def ex_add_server_firewall_policy(self, server_id, ip_id, firewall_id):
        """
        Adds a firewall policy to the server's IP address

        :param server_id: Id of the server
        :type: ``str``

        :param ip_id: ID of the IP address
        :type: ``str``

        :param firewall_id: ID of the firewall policy
        :type: ``str``

        :return: ``Node`` instance
        :rtype: ``Node``
        """
        body = {
            'id': firewall_id
        }
        response = self.connection.request(
            action='/servers/%s/ips/%s/firewall_policy' % (server_id, ip_id),
            data=body,
            method='POST'
        )

        return self._to_node(response.object)

    """
    Firewall Policy operations
    """

    def ex_create_firewall_policy(self, name, rules, description=None):
        """
        Creates a firewall Policy.

        :param name:
        :param description:
        :param rules:

        :rtype: `dict`
        :return: `dict` firewall policy

        """
        body = {
            'name': name
        }

        if description is not None:
            body['description'] = description

        if len(rules) == 0:
            raise ValueError(
                'At least one firewall rule is required.'
            )
        else:
            body['rules'] = rules

        response = self.connection.request(
            action='firewall_policies',
            data=body,
            method='POST',
        )

        return response.object

    def ex_list_firewall_policies(self):
        """"
        List firewall policies

        :return: 'dict'
        """

        response = self.connection.request(
            action='firewall_policies',
            method='GET'
        )

        return response.object

    def ex_get_firewall_policy(self, fw_id):
        """
        Gets firewall policy

        :param fw_id: ID of the firewall policy
        :return: 'dict'
        """

        response = self.connection.request(
            action='firewall_policy/%s' % fw_id,
            method='GET'
        )

        return response.object

    def ex_delete_firewall_policy(self, fw_id):
        """
        Deletes firewall policy

        :param fw_id: ID of the Firewall
        :return: 'dict'
        """
        response = self.connection.request(
            action='firewall_policy/%s' % fw_id,
            method='DELETE'
        )

        return response.object

    """
    Shared storage operations
    """

    def ex_list_shared_storages(self):
        """
        List of shared storages
        :return: 'dict'
        """
        response = self.connection.request(
            action='shared_storages',
            method='GET'
        )

        return response.object

    def ex_get_shared_storage(self, storage_id):
        """
        Gets a shared storage
        :return: 'dict'
        """
        response = self.connection.request(
            action='shared_storages/%s' % (storage_id),
            method='GET'
        )

        return response.object

    def ex_create_shared_storage(self, name, size, datacenter_id=None,
                                 description=None):
        """
        Creates a shared storage
        :param name: Name of the storage
        :param size: Size of the storage
        :param datacenter_id: datacenter where storage should be created
        :param description: description ot the  storage
        :return: 'dict'
        """

        body = {
            'name': name,
            'size': size,
            'datacenter_id': datacenter_id
        }

        if description is not None:
            body['description'] = description

        response = self.connection.request(
            action='shared_storages',
            data=body,
            method='POST'
        )

        return response.object

    def ex_delete_shared_storage(self, storage_id):
        """
        Removes a shared storage

        :param storage_id: Id of the shared storage
        :type: ``str``

        :return: Instnace of shared storage
        :rtype: ``list`` of ``dict``
        """
        response = self.connection.request(
            action='shared_storages/%s' % storage_id,
            method='DELETE'
        )

        return response.object

    def ex_attach_server_to_shared_storage(self, storage_id,
                                           server_id, rights):
        """
        Attaches a single server to a shared storage

        :param storage_id: Id of the shared storage
        :param server_id: Id of the server to be attached to the shared storage
        :param rights:
        :return:
        :rtype: 'dict'
        """
        body = {
            'severs': [
                {
                    'id': server_id,
                    'rights': rights
                }
            ]
        }

        response = self.connection.request(
            action='shared_storages/%s/servers' % storage_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_get_shared_storage_server(self, storage_id, server_id):
        """
        Gets a shared storage's server
        :param storage_id:
        :param server_id:
        :return:
        """
        response = self.connection.request(
            action='shared_storages/%s/servers/%s' % (storage_id, server_id),
        )

        return response.object

    def ex_detach_server_from_shared_storage(self, storage_id,
                                             server_id):
        """
        Detaches a server from shared storage

        :param storage_id: Id of the shared storage
        :type: ``str``

        :param server_id: Id of the server
        :type: ``str``

        :return: Instance of shared storage
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='shared_storages/%s/servers/%s' % (storage_id, server_id),
            method='DELETE'
        )

        return response.object

    """
    Load Balancers operations
    """

    def ex_create_load_balancer(self, name, method, rules,
                                persistence=None,
                                persistence_time=None,
                                health_check_test=None,
                                health_check_interval=None,
                                health_check_path=None,
                                health_check_parser=None,
                                datacenter_id=None,
                                description=None):
        """

        :param name: Name of the load balancer

        :param method: Load balancer method

        :param rules: Load balancer rules
        :type rules: ``list`` of ``dict``

        :param persistence: Indictes if persistance is set
        :type persistence: ``boolean``

        :param persistence_time: Persistance time
        :type persistence_time: ``int``

        :param health_check_test: Type of test
        :type health_check_test:``str``

        :param health_check_interval: Interval of the check

        :param health_check_path: Path
        :type health_check_path: ``str``

        :param health_check_parser: Parser
        :type health_check_parser:``str``

        :param datacenter_id: Data center id
        :type datacenter_id:``str``

        :param description: Description of load balancer
        :type description:``str``

        :return: ``dict``
        """

        body = {
            'name': name,
            'method': method,
        }

        body['rules'] = []
        body['rules'] = rules

        if persistence is not None:
            body['persistence'] = persistence
        if persistence_time is not None:
            body['persistence_time'] = persistence_time
        if health_check_test is not None:
            body['health_check_test'] = health_check_test
        if health_check_interval is not None:
            body['health_check_interval'] = health_check_interval
        if health_check_path is not None:
            body['health_check_path'] = health_check_path
        if health_check_parser is not None:
            body['health_check_parser'] = health_check_parser
        if datacenter_id is not None:
            body['datacenter_id'] = datacenter_id
        if description is not None:
            body['description'] = description

        response = self.connection.request(
            action='load_balancers',
            data=body,
            method='POST'
        )

        return response.object

    def ex_update_load_balancer(self, lb_id, name=None, description=None,
                                health_check_test=None,
                                health_check_interval=None,
                                persistence=None,
                                persistence_time=None,
                                method=None):
        body = {}

        if name is not None:
            body['name'] = name
        if description is not None:
            body['description'] = description
        if health_check_test is not None:
            body['health_check_test'] = health_check_test
        if health_check_interval is not None:
            body['health_check_interval'] = health_check_interval
        if persistence is not None:
            body['persistence'] = persistence
        if persistence_time is not None:
            body['persistence_time'] = persistence_time
        if method is not None:
            body['method'] = method

        response = self.connection.request(
            action='load_balancers/%s' % lb_id,
            data=body,
            method='PUT'
        )

        return response.object

    def ex_add_servers_to_load_balancer(self, lb_id, server_ips=[]):
        """
        Adds server's IP address to load balancer

        :param lb_id: Load balancer ID
        :type: ``str``

        :param server_ips: Array of server IP IDs
        :type: ``list`` of ``str``

        :return: Instance of load balancer
        :rtype: ``dict``
        """
        body = {
            'server_ips': server_ips,
        }

        response = self.connection.request(
            action='load_balancers/%s/server_ips' % lb_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_remove_server_from_load_balancer(self, lb_id, server_ip):
        """
        Removes server's IP from load balancer

        :param lb_id: Load balancer ID
        :type: ``str``

        :param server_ip: ID of the server IP
        :type: ``str``

        :return: Instance of load balancer
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='/load_balancers/%s/server_ips/%s' % (lb_id, server_ip),
            method='DELETE'
        )

        return response.object

    def ex_add_load_balancer_rule(self, lb_id, protocol, port_balancer,
                                  port_server, source=None):
        """
        Adds a rule to load balancer

        :param lb_id: Load balancer ID
        :rtype: ``str``

        :param protocol: Load balancer protocol
        :rtype: ``str``

        :param port_balancer: Port to be balananced
        :rtype: ``int``

        :param port_server: Server port
        :rtype: ``int``

        :param source: Source IP address
        :rtype: ``str``

        :return: Instance of load balancer
        :rtype: ``dict``
        """

        body = {
            'rules': [
                {
                    'protocol': protocol,
                    'port_balancer': port_balancer,
                    'port_server': port_server
                }
            ]
        }

        if source is not None:
            body['rules'][0]['source'] = source

        response = self.connection.request(
            action='/load_balancers/%s/rules' % lb_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_remove_load_balancer_rule(self, lb_id, rule_id):
        """
        Removes load balancer rule

        :param lb_id: Load balancer ID
        :rtype: ``str``

        :param rule_id: Rule ID
        :rtype: ``str``

        :return: Instance of load balancer
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='/load_balancers/%s/rules/%s' % (lb_id, rule_id),
            method='DELETE'
        )

        return response.object

    def ex_list_load_balancers(self):
        """
        Lists all load balancers

        :return: List of load balancers
        :rtype: ``list`` of ``dict``
        """
        response = self.connection.request(
            action='load_balancers',
            method='GET'
        )
        return response.object

    def ex_get_load_balancer(self, lb_id):
        """
        Gets a single load balancer

        :param lb_id: ID of the load balancer
        :type lb_id: ``str``

        :return: Instance of load balancer
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='load_balancers/%s' % lb_id,
            method='GET'
        )

        return response.object

    def ex_list_load_balancer_server_ips(self, lb_id):
        """
        List balanced server IP addresses

        :param lb_id: ID of the load balancer
        :type lb_id: ``str``

        :return: Array of IP address IDs
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='load_balancers/%s/server_ips' % lb_id,
            method='GET'
        )

        return response.object

    def ex_get_load_balancer_server_ip(self, lb_id, server_ip):
        """
        Gets load balanced server id

        :param lb_id: ID of the load balancer
        :type lb_id: ``str``

        :param server_ip: ID of the server IP
        :type server_ip: ``str``

        :return: Server IP
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='load_balancers/%s/server_ips/%s' % (lb_id, server_ip),
            method='GET'
        )

        return response.object

    def ex_list_load_balancer_rules(self, lb_id):
        """
        Lists loadbalancer rules

        :param lb_id: ID of the load balancer
        :type lb_id: ``str``

        :return: Lists of rules
        :rtype: ``list`` of ``dict``
        """
        response = self.connection.request(
            action='load_balancers/%s/rules' % lb_id,
            method='GET'
        )

        return response.object

    def ex_get_load_balancer_rule(self, lb_id, rule_id):
        """
        Get a load balancer rule

        :param lb_id: ID of the load balancer
        :type lb_id: ``str``

        :param rule_id: Rule ID
        :type rule_id: ``str``

        :return: A load balancer rule
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='load_balancers/%s/rules/%s' % (lb_id, rule_id),
            method='GET'
        )

        return response.object

    def ex_delete_load_balancer(self, lb_id):
        """
        Deletes a load balancer rule

        :param lb_id: ID of the load balancer
        :type lb_id: ``str``

        :param rule_id: Rule ID
        :type rule_id: ``str``

        :return: Instance of load balancer
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='load_balancers/%s' % lb_id,
            method='DELETE'
        )

        return response.object

    """
    Public IP operations
    """

    def ex_list_public_ips(self):
        """
        Lists all public IP addresses

        :return: Array of public addresses
        :rtype: ``list`` of ``dict``
        """
        response = self.connection.request(
            action='public_ips',
            method='GET'
        )

        return response.object

    def ex_create_public_ip(self, type, reverse_dns=None, datacenter_id=None):
        """
        Creates a public IP

        :param type: Type of IP (IPV4 or IPV6)
        :type type: ``str``

        :param reverse_dns: Reverse DNS
        :type reverse_dns: ``str``

        :param datacenter_id: Datacenter ID where IP address will be crated
        :type datacenter_id: ``str``

        :return: Instance of Public IP
        :rtype: ``dict``
        """
        body = {
            'type': type
        }

        if reverse_dns is not None:
            body['reverse_dns'] = reverse_dns
        if datacenter_id is not None:
            body['datacenter_id'] = datacenter_id

        response = self.connection.request(
            action='public_ips',
            data=body,
            method='POST'
        )

        return response.object

    def ex_get_public_ip(self, ip_id):
        """
        Gets a Public IP

        :param ip_id: ID of the IP
        :type ip_id: ``str``

        :return: Instance of Public IP
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='public_ips/%s' % ip_id,
            method='GET'
        )

        return response.object

    def ex_delete_public_ip(self, ip_id):
        """
        Deletes a public IP

        :param ip_id: ID of public IP
        :type ip_id: ``str``

        :return: Instance of IP Address
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='public_ips/%s' % ip_id,
            method='DELETE'
        )

        return response

    def ex_update_public_ip(self, ip_id, reverse_dns):
        """
        Updates a Public IP

        :param ip_id: ID of public IP
        :type ip_id: ``str``

        :param reverse_dns: Reverse DNS
        :type reverse_dns: ``str``

        :return: Instance of Public IP
        :rtype: ``dict``
        """

        body = {
            'reverse_dns': reverse_dns
        }
        response = self.connection.request(
            action='public_ips/%s' % ip_id,
            data=body,
            method='DELETE'
        )

        return response.object

    """
    Private Network Operations
    """

    def ex_list_private_networks(self):
        """
        Lists all private networks

        :return: List of private networks
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='private_networks',
            method='GET'
        )

        return response.object

    def ex_create_private_network(self, name, description=None,
                                  datacenter_id=None,
                                  network_address=None,
                                  subnet_mask=None):
        """
        Creates a private network

        :param name: Name of the private network
        :type name: ``str``

        :param description: Description of the private network
        :type description: ``str``

        :param datacenter_id: ID of the data center for the private network
        :type datacenter_id: ``str``

        :param network_address: Network address of the private network
        :type network_address: ``str``

        :param subnet_mask: Subnet mask of the private network
        :type subnet_mask: ``str``

        :return: Newly created private network
        :rtype: ``dict``
        """

        body = {
            'name': name
        }

        if description is not None:
            body['description'] = description
        if datacenter_id is not None:
            body['datacenter_id'] = datacenter_id
        if network_address is not None:
            body['network_address'] = network_address
        if subnet_mask is not None:
            body['subnet_maks'] = subnet_mask

        response = self.connection.request(
            action='private_networks',
            data=body,
            method='POST'
        )
        return response.object

    def ex_delete_private_network(self, network_id):
        """
        Deletes a private network

        :param network_id: Id of the private network
        :type network_id: ``str``

        :return: Instance of the private network being deleted
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='private_networks' % network_id,
            method='DELETE'
        )

        return response.object

    def ex_update_private_network(self, network_id,
                                  name=None, description=None,
                                  datacenter_id=None,
                                  network_address=None,
                                  subnet_mask=None):
        """
       Updates a private network

       :param name: Name of the private network
       :type name: ``str``

       :param description: Description of the private network
       :type description: ``str``

       :param datacenter_id: ID of the data center for the private network
       :type datacenter_id: ``str``

       :param network_address: Network address of the private network
       :type network_address: ``str``

       :param subnet_mask: Subnet mask of the private network
       :type subnet_mask: ``str``

       :return: Instance of private network
       :rtype: ``dict``
       """
        body = {}

        if name is not None:
            body['name'] = name
        if description is not None:
            body['description'] = description
        if datacenter_id is not None:
            body['datacenter_id'] = datacenter_id
        if network_address is not None:
            body['network_address'] = network_address
        if subnet_mask is not None:
            body['subnet_maks'] = subnet_mask

        response = self.connection.request(
            action='private_networks/%s',
            data=body,
            method='PUT'
        )

        return response.object

    def ex_list_private_network_servers(self, network_id):
        """
        Lists all private network servers

        :param network_id: Private network ID
        :type network_id: ``str``

        :return: List of private network servers
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='/private_networks/%s/servers' % network_id,
            method='GET'
        )
        return response.object

    def ex_add_private_network_server(self, network_id, server_ids):
        """
        Add servers to private network

        :param network_id: Private Network ID
        :type network_id: ``str``

        :param server_ids: List of server IDs
        :type server_ids: ``list`` of ``str``

        :return: List of attached servers
        :rtype: ``dict``

        """
        body = {
            'servers': server_ids

        }

        response = self.connection.request(
            action='/private_networks/%s/servers' % network_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_remove_server_from_private_network(self, network_id, server_id):
        """
        Removes a server from the private network

        :param network_id: Private Network ID
        :type network_id: ``str``

        :param server_id: Id of the server
        :type server_id: ``str``

        :return: Instance of the private network
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='/private_networks/%s/servers/%s' % (network_id, server_id),
            method='POST'
        )
        return response.object

    """
    Monitoring policy operations
    """

    def ex_list_monitoring_policies(self):
        """
        Lists all monitoring policies

        :return: List of monitoring policies
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='monitoring_policies',
            method='GET'
        )

        return response.object

    def ex_create_monitoring_policy(self, name, thresholds,
                                    ports,
                                    processes,
                                    description=None,
                                    email=None,
                                    agent=None,
                                    ):
        """
        Creates a monitoring policy

        :param name: Name for the monitoring policy
        :type name: ``str``

        :param thresholds: Thresholds for the monitoring policy
        :type thresholds: ``dict``

        :param ports: Monitoring policies for ports
        :type ports: ``list`` of ``dict``

        :param processes: Processes to be monitored
        :type processes: ``list`` of ``dict``

        :param description: Description for the monitoring policy
        :type description: ``str``

        :param email: Email for notifications
        :type email: ``str``

        :param agent: Indicates if agent application will be installed
        :type agent: ``boolean``

        :return: Newly created instance of monitofing policy
        :rtype: ``dict``
        """
        body = {
            'name': name,
            'thresholds': thresholds,
            'ports': ports,
            'processes': processes
        }

        if description is not None:
            body['description'] = description
        if email is not None:
            body['email'] = email
        if agent is not None:
            body['agent'] = agent

        response = self.connection.request(
            action='monitoring_policies',
            data=body,
            method='POST'
        )
        return response.object

    def ex_delete_monitoring_policy(self, policy_id):
        """
        Deletes a monitoring policy

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :return: Instance of the monitoring policy being deleted
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='monitoring_policies' % policy_id,
            method='DELETE'
        )

        return response.object

    def ex_update_monitoring_policy(self, policy_id,
                                    email,
                                    thresholds,
                                    name=None, description=None):
        """
        Updates monitoring policy

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param email: Email to send notifications to
        :type email: ``str``

        :param thresholds: Thresholds for the monitoring policy
        :type thresholds: ``dict``

        :param name: Name of the monitoring policy
        :type name: ``str``

        :param description: Description of the monitoring policy
        :type description: ``str``

        :return: Instance of the monitoring policy being deleted
        :rtype: ``dict``
        """

        body = {}

        if name is not None:
            body['name'] = name
        if description is not None:
            body['description'] = description
        if thresholds is not None:
            body['thresholds'] = thresholds
        if email is not None:
            body['email'] = email

        response = self.connection.request(
            action='monitoring_policies/%s' % policy_id,
            data=body,
            method='PUT'
        )

        return response.object

    def ex_get_monitoring_policy(self, policy_id):
        """
        Fetches a monitoring policy

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='monitoring_policies/%s' % policy_id,
            method='GET'
        )

        return response.object

    def ex_get_monitoring_policy_ports(self, policy_id):
        """
        Fetches monitoring policy ports

        :param policy_id: Id of the monitoring policy
        :type policy_id:

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/ports' % policy_id,
            method='GET'
        )

        return response.object

    def ex_get_monitoring_policy_port(self, policy_id, port_id):
        """
        Fetches monitoring policy port

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param port_id: Id of the port
        :type port_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/ports/%s' % (policy_id, port_id),
            method='GET'
        )

        return response.object

    def ex_remove_monitoring_policy_port(self, policy_id, port_id):
        """
        Removes monitoring policy port

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param port_id: Id of the port
        :type port_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/ports/%s' % (policy_id, port_id),
            method='DELETE'
        )

        return response.object

    def ex_add_monitoring_policy_ports(self, policy_id, ports):
        """
        Add monitoring policy ports

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param ports: List of ports
        :type ports: ``dict``
        [
           {
              'protocol':'TCP',
              'port':'80',
              'alert_if':'RESPONDING',
              'email_notification':true
           }
        ]

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        body = {'ports': ports}

        response = self.connection.request(
            action='monitoring_policies/%s/ports' % policy_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_get_monitoring_policy_processes(self, policy_id):
        """
        Fetches monitoring policy processes

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/processes' % policy_id,
            method='GET'
        )

        return response.object

    def ex_get_monitoring_policy_process(self, policy_id, process_id):
        """
        Fetches monitoring policy process

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param process_id: Id of the process
        :type process_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/processes/%s'
                   % (policy_id, process_id),
            method='GET'
        )

        return response.object

    def ex_remove_monitoring_policy_process(self, policy_id, process_id):
        """
        Removes monitoring policy process

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param process_id: Id of the process
        :type process_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/processes/%s'
                   % (policy_id, process_id),
            method='DELETE'
        )

        return response.object

    def ex_add_monitoring_policy_processes(self, policy_id, processes):
        """
        Add monitoring policy processes

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param processes: List of processes
        :type processes: ``list`` of ``dict``
        [
          {
            'process': 'taskmmgr',
            'alert_if': 'RUNNING',
            'email_notification': true
          }
        ]

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """

        body = {'processes': processes}

        response = self.connection.request(
            action='monitoring_policies/%s/processes' % policy_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_list_monitoring_policy_servers(self, policy_id):
        """
        List all servers that are being monitoried by the policy

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :return: List of servers being monitored
        :rtype: ``list`` of ``dict``
        """

        response = self.connection.request(
            action='monitoring_policies/%s/servers' % policy_id,
            method='GET'
        )

        return response.object

    def ex_add_servers_to_monitoring_policy(self, policy_id, servers):
        """
        Adds servers to monitoring policy

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param servers: List of server ID
        :type servers: ``list`` of ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """
        body = {
            'servers': servers
        }
        response = self.connection.request(
            action='monitoring_policies/%s/servers' % policy_id,
            data=body,
            method='POST'
        )

        return response.object

    def ex_remove_server_from_monitoring_policy(self, policy_id, server_id):
        """
        Removes a server from monitoring policy

        :param policy_id: Id of the monitoring policy
        :type policy_id: ``str``

        :param server_id: Id of the server
        :type server_id: ``str``

        :return: Instance of a monitoring policy
        :rtype: ``dict``
        """
        response = self.connection.request(
            action='monitoring_policies/%s/servers/%s'
                   % (policy_id, server_id),
            method='DELETE'
        )

        return response.object

    """
    Private Functions
    """

    def _to_images(self, object, image_type=None):
        if image_type is not None:
            images = [image for image in object if image['type'] == image_type]
        else:
            images = [image for image in object]

        return [self._to_image(image) for image in images]

    def _to_image(self, data):
        extra = {
            'os_family': data['os_family'],
            'os': data['os'],
            'os_version': data['os_version'],
            'os_architecture': data['os_architecture'],
            'os_image_type': data['os_image_type'],
            'min_hdd_size': data['min_hdd_size'],
            'available_datacenters': data['available_datacenters'],
            'licenses': data['licenses'],
            'version': data['version'],
            'categories': data['categories']
        }
        return NodeImage(id=data['id'], name=data['name'], driver=self,
                         extra=extra)

    def _to_node_size(self, data):
        return NodeSize(
            id=data['id'],
            name=data['name'],
            ram=data['hardware']['ram'],
            disk=data['hardware']['hdds'][0]['size'],
            bandwidth=None,
            price=None,
            driver=self.connection.driver,
            extra={
                'vcores': data['hardware']['vcore'],
                'cores_per_processor': data['hardware']['cores_per_processor']}

        )

    def _to_location(self, location):
        return NodeLocation(
            id=location['id'],
            name=location['country_code'],
            country=location['location'],
            driver=self.connection.driver
        )

    def _to_nodes(self, servers):
        return [self._to_node(
            server) for server in servers]

    def _to_node(self, server):
        extra = {}
        extra['datacenter'] = server['datacenter']

        if 'description' in server:
            extra['description'] = server['description']
        if 'status' in server:
            extra['status'] = server['status']
        if 'image' in server:
            extra['image'] = server['image']
        if 'hardware' in server:
            extra['hardware'] = server['hardware']
        if 'dvd' in server:
            extra['dvd'] = server['dvd']
        if 'snapshot' in server:
            extra['snapshot'] = server['snapshot']
        if 'ips' in server:
            extra['ips'] = server['ips']
        if 'alerts' in server:
            extra['alerts'] = server['alerts']
        if 'monitoring_policy' in server:
            extra['monitoring_policy'] = server['monitoring_policy']
        if 'private_networks' in server:
            extra['private_networks'] = server['private_networks']

        ips = []

        if server['ips'] is not None:
            for ip in server['ips']:
                ips.append(ip['ip'])
        state = self.NODE_STATE_MAP.get(
            server['status']['state'])

        return Node(
            id=server['id'],
            state=state,
            name=server['name'],
            driver=self.connection.driver,
            public_ips=ips,
            private_ips=None,
            extra=extra
        )

    def _wait_for_state(self, server_id, state, retries=50):
        for i in (0, retries):
            server = self.ex_get_server(server_id)

            if server.extra['status']['state'] == state:
                return
            sleep(5)

            if i == retries:
                raise Exception('Retries count reached')

    def _list_fixed_instances(self):
        response = self.connection.request(
            action='/servers/fixed_instance_sizes',
            method='GET'
        )

        return response.object
