# 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 time import sleep

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

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 = "{} (code: {})".format(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().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["fixed_instance_size_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/{}/hardware/hdds/{}".format(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/{}/hardware/hdds/{}".format(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/{}/ips/{}".format(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/{}/ips/{}".format(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/{}/ips/{}/firewall_policy".format(server_id, ip_id),
            method="GET",
        )

        return 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/{}/ips/{}/firewall_policy".format(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/{}/servers/{}".format(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/{}/servers/{}".format(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/{}/server_ips/{}".format(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/{}/rules/{}".format(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/{}/server_ips/{}".format(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/{}/rules/{}".format(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/{}/servers/{}".format(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/{}/ports/{}".format(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/{}/ports/{}".format(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/{}/processes/{}".format(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/{}/processes/{}".format(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/{}/servers/{}".format(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
