# 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.

import time

from libcloud.compute.base import (
    Node,
    KeyPair,
    NodeImage,
    NodeState,
    NodeDriver,
    NodeLocation,
    StorageVolume,
    VolumeSnapshot,
    StorageVolumeState,
)
from libcloud.utils.iso8601 import parse_date
from libcloud.common.gridscale import GridscaleBaseDriver, GridscaleConnection
from libcloud.compute.providers import Provider


class GridscaleIp:
    """
    Ip Object

    :param id: uuid
    :type id: ``str``
    :param family: family of ip (v4 or v6)
    :type family: ``str``
    :param prefix: prefix of ip
    :type prefix: ``str``
    :param ip_address: Ip address
    :type ip_address: ``str``
    :param create_time: Time ip was created
    :type create_time: ``str``
    """

    def __init__(self, id, family, prefix, create_time, address, extra=None):
        self.id = id
        self.family = family
        self.prefix = prefix
        self.create_time = create_time
        self.ip_address = address
        self.extra = extra or {}

    def __repr__(self):
        return "Ip: id={}, family={}, prefix={}, create_time={}, " "ip_address={}".format(
            self.id, self.family, self.prefix, self.create_time, self.ip_address
        )


class GridscaleNetwork:
    """
    Network Object

    :param id: uuid
    :type id: ``str``
    :param name: Name of Network
    :type name: ``str``
    :param status: Network status
    :type status: ``str``
    :param relations: object related to network
    :type relations: ``object``
    :param create_time: Time Network was created
    :type create_time: ``str``
    """

    def __init__(self, id, name, status, create_time, relations):
        self.id = id
        self.name = name
        self.status = status
        self.create_time = create_time
        self.relations = relations

    def __repr__(self):
        return "Network: id={}, name={}, status={}, create_time={}, " "relations={}".format(
            self.id, self.name, self.status, self.create_time, self.relations
        )


class GridscaleNodeDriver(GridscaleBaseDriver, NodeDriver):
    """
    create and entry in libcloud/compute/providers for gridscale
    """

    connectionCls = GridscaleConnection
    type = Provider.GRIDSCALE
    name = "Gridscale"
    api_name = "gridscale"
    website = "https://gridscale.io"
    features = {"create_node": ["ssh_key"]}

    def __init__(self, user_id, key, **kwargs):
        super().__init__(user_id, key, **kwargs)

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

        :return: List of node objects
        :rtype: ``list`` of :class:`.Node`
        """
        result = self._sync_request(data=None, endpoint="objects/servers/")
        nodes = []
        for key, value in self._get_response_dict(result).items():
            node = self._to_node(value)
            nodes.append(node)

        return sorted(nodes, key=lambda sort: sort.created_at)

    def list_locations(self):
        """
        List all available data centers.

        :return: List of node location objects
        :rtype: ``list`` of :class:`.NodeLocation`
        """
        locations = []
        result = self._sync_request(endpoint="objects/locations/")
        for key, value in self._get_response_dict(result).items():
            location = self._to_location(value)
            locations.append(location)

        return sorted(locations, key=lambda nod: nod.id)

    def list_volumes(self):
        """
        List all volumes.

        :return: List of StorageVolume object
        :rtype: ``list`` of :class:`.StorageVolume`
        """
        volumes = []
        result = self._sync_request(endpoint="objects/storages/")
        for key, value in self._get_response_dict(result).items():
            volume = self._to_volume(value)
            volumes.append(volume)
        return sorted(volumes, key=lambda sort: sort.extra["create_time"])

    def ex_list_networks(self):
        """
        List all networks.

        :return: List of objects.
        :rtype: ``list`` of :class:`.GridscaleNetwork`
        """
        networks = []
        result = self._sync_request(endpoint="objects/networks/")
        for key, value in self._get_response_dict(result).items():
            network = self._to_network(value)
            networks.append(network)
        return sorted(networks, key=lambda sort: sort.create_time)

    def list_volume_snapshots(self, volume):
        """
        Lists all snapshots for storage volume.

        :param volume: storage the snapshot is attached to
        :type volume: :class:`.StorageVolume`

        :return: Snapshots
        :rtype: ``list`` of :class:`.VolumeSnapshot`
        """
        snapshots = []
        result = self._sync_request(endpoint="objects/storages/" "{}/snapshots".format(volume.id))
        for key, value in self._get_response_dict(result).items():
            snapshot = self._to_volume_snapshot(value)
            snapshots.append(snapshot)
        return sorted(snapshots, key=lambda snapshot: snapshot.created)

    def ex_list_ips(self):
        """
        Lists all IPs available.

        :return: List of IP objects.
        :rtype: ``list`` of :class:`.GridscaleIp`
        """
        ips = []
        result = self._sync_request(endpoint="objects/ips/")
        for key, value in self._get_response_dict(result).items():
            ip = self._to_ip(value)
            ips.append(ip)
        return ips

    def list_images(self):
        """
        List images.

        :return: List of node image objects
        :rtype: ``list`` of :class:`.NodeImage`
        """
        templates = []
        result = self._sync_request(endpoint="objects/templates")
        for key, value in self._get_response_dict(result).items():
            template = self._to_node_image(value)
            templates.append(template)
        return sorted(templates, key=lambda sort: sort.name)

    def create_node(self, name, size, image, location, ex_ssh_key_ids=None, **kwargs):
        """
        Create a simple node  with a name, cores, memory at the designated
        location.

        :param name: Name of the server.
        :type name: ``str``

        :param size: Nodesize object.
        :type size: :class:`.NodeSize`

        :param image: OS image to attach to the storage.
        :type image: :class:`.GridscaleTemplate`

        :param location: The data center to create a node in.
        :type location: :class:`.NodeLocation`

        :keyword ex_ssh_key_ids: List of SSH key IDs to add to the server.
        :type ex_ssh_key_ids: ``list`` of ``str``

        :return: The newly created Node.
        :rtype: :class:`.Node`

        """

        if size.ram % 1024 != 0:
            raise Exception("Value not accepted. Use a multiple of 1024 e.g." "1024, 2048, 3072...")
        data = {
            "name": name,
            "cores": size.extra["cores"],
            "memory": int(size.ram / 1024),
            "location_uuid": location.id,
        }
        self.connection.async_request("objects/servers/", data=data, method="POST")

        node = self._to_node(
            self._get_resource(
                "servers", self.connection.poll_response_initial.object["object_uuid"]
            )
        )

        volume = self._create_volume_from_template(
            name=image.extra["ostype"],
            size=size.disk,
            location=location,
            template={"template_uuid": image.id, "sshkeys": ex_ssh_key_ids},
        )

        ip = self.ex_create_ip(4, location, name + "_ip")

        self.attach_volume(node, volume)
        self.ex_link_ip_to_node(node, ip)
        self.ex_link_network_to_node(node, self.ex_list_networks()[0])
        self.ex_start_node(node)

        return self._to_node(self._get_resource("servers", node.id))

    def ex_create_ip(self, family, location, name):
        """
        Create either an ip_v4 ip or a ip_v6.

        :param family: Defines if the ip is v4 or v6 with int 4 or int 6.
        :type family: ``int``

        :param location: Defines which datacenter the created ip
                         responds with.
        :type location: :class:`.NodeLocation`

        :param name: Name of your Ip.
        :type name: ``str``

        :return: Ip
        :rtype: :class:`.GridscaleIp`
        """
        self.connection.async_request(
            "objects/ips/",
            data={"name": name, "family": family, "location_uuid": location.id},
            method="POST",
        )

        return self._to_ip(
            self._get_resource("ips", self.connection.poll_response_initial.object["object_uuid"])
        )

    def ex_create_networks(self, name, location):
        """
        Create a network at the data center location.

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

        :param location: Location.
        :type location: :class:`.NodeLocation`

        :return: Network.
        :rtype: :class:`.GridscaleNetwork`
        """
        self.connection.async_request(
            "objects/networks",
            data={"name": name, "location_uuid": location.id},
            method="POST",
        )

        return self._to_network(
            self._get_resource(
                "network", self.connection.poll_response_initial.object["object_uuid"]
            )
        )

    def create_volume(self, size, name, location=None, snapshot=None):
        """
        Create a new volume.

        :param size: Integer in GB.
        :type size: ``int``

        :param name: Name of the volume.
        :type name: ``str``

        :param location: The server location.
        :type location: :class:`.NodeLocation`

        :param snapshot:  Snapshot from which to create the new
                          volume.  (optional)
        :type snapshot: :class:`.VolumeSnapshot`

        :return: Newly created StorageVolume.
        :rtype: :class:`.StorageVolume`
        """

        return self._create_volume_from_template(size, name, location)

    def _create_volume_from_template(self, size, name, location=None, template=None):
        """
        create Storage

        :param name: name of your Storage unit
        :type name: ``str``

        :param size: Integer in GB.
        :type size: ``int``

        :param location: your server location
        :type location: :class:`.NodeLocation`

        :param template: template to shape the storage capacity to
        :type template: ``dict``

        :return: newly created StorageVolume
        :rtype: :class:`.GridscaleVolumeStorage`
        """
        template = template
        self.connection.async_request(
            "objects/storages/",
            data={
                "name": name,
                "capacity": size,
                "location_uuid": location.id,
                "template": template,
            },
            method="POST",
        )

        return self._to_volume(
            self._get_resource(
                "storages", self.connection.poll_response_initial.object["object_uuid"]
            )
        )

    def create_volume_snapshot(self, volume, name):
        """
        Creates a snapshot of the current state of your volume,
        you can rollback to.

        :param volume: Volume you want to create a snapshot of.
        :type volume: :class:`.StorageVolume`

        :param name: Name of the snapshot.
        :type name: ``str``

        :return: VolumeSnapshot.
        :rtype: :class:`.VolumeSnapshot`
        """
        self.connection.async_request(
            "objects/storages/{}/snapshots".format(volume.id),
            data={"name": name},
            method="POST",
        )

        return self._to_volume_snapshot(
            self._get_resource(
                "storages/{}/snapshots".format(volume.id),
                self.connection.poll_response_initial.object["object_uuid"],
            )
        )

    def create_image(self, node, name):
        """
        Creates an image from a node object.

        :param node: Node to run the task on.
        :type node: :class:`.Node`

        :param name: Name for new image.
        :type name: ``str``

        :return: NodeImage.
        :rtype: :class:`.NodeImage`
        """
        storage_dict = node.extra["relations"]["storages"][0]
        snapshot_uuid = ""
        if storage_dict["bootdevice"] is True:
            self.connection.async_request(
                "objects/storages/{}/snapshots/".format(storage_dict["object_uuid"]),
                data={"name": name + "_snapshot"},
                method="POST",
            )

            snapshot_uuid = self.connection.poll_response_initial.object["object_uuid"]

            self.connection.async_request(
                "objects/templates/",
                data={"name": name, "snapshot_uuid": snapshot_uuid},
                method="POST",
            )

            snapshot_dict = self._get_response_dict(
                self._sync_request(
                    endpoint="objects/storages/{}/snapshots/{}".format(
                        storage_dict["object_uuid"], snapshot_uuid
                    )
                )
            )

            self.destroy_volume_snapshot(self._to_volume_snapshot(snapshot_dict))

        return self._to_node_image(
            self._get_resource(
                "templates", self.connection.poll_response_initial.object["object_uuid"]
            )
        )

    def destroy_node(self, node, ex_destroy_associated_resources=False):
        """
        Destroy node.

        :param node: Node object.
        :type node: :class:`.Node`

        :param ex_destroy_associated_resources: True to destroy associated
        resources such as storage volumes and IPs.
        :type ex_destroy_associated_resources: ``bool``

        :return: True if the destroy was successful, otherwise False.
        :rtype: ``bool``
        """
        if ex_destroy_associated_resources:
            associated_volumes = self.ex_list_volumes_for_node(node=node)
            associated_ips = self.ex_list_ips_for_node(node=node)

        # 1. Delete the server itself
        result = self._sync_request(endpoint="objects/servers/{}".format(node.id), method="DELETE")

        # 2. Destroy associated resouces (if requested)
        if ex_destroy_associated_resources:
            for volume in associated_volumes:
                self.destroy_volume(volume=volume)

            for ip in associated_ips:
                self.ex_destroy_ip(ip=ip)

        return result.status == 204

    def destroy_volume(self, volume):
        """
        Delete volume.

        :param volume: Volume to be destroyed.
        :type volume: :class:`.StorageVolume`

        :return: True if the destroy was successful, otherwise False.
        :rtype: ``bool``
        """
        result = self._sync_request(
            endpoint="objects/storages/{}".format(volume.id), method="DELETE"
        )
        return result.status == 204

    def ex_destroy_ip(self, ip):
        """
        Delete an ip.

        :param ip: IP object.
        :type ip: :class:`.GridscaleIp`

        :return: ``True`` if delete_image was successful, ``False`` otherwise.
        :rtype: ``bool``
        """
        result = self._sync_request(endpoint="objects/ips/{}".format(ip.id), method="DELETE")
        return result.status == 204

    def destroy_volume_snapshot(self, snapshot):
        """
        Destroy a snapshot.

        :param snapshot: The snapshot to delete.
        :type snapshot: :class:'.VolumeSnapshot`

        :return: True if the destroy was successful, otherwise False.
        :rtype: ``bool``
        """
        result = self._sync_request(
            endpoint="objects/storages/"
            "{}/snapshots/{}/".format(snapshot.extra["parent_uuid"], snapshot.id),
            method="DELETE",
        )
        return result.status == 204

    def ex_destroy_network(self, network):
        """
        Delete network.

        :param network: Network object.
        :type network: :class:`.GridscaleNetwork`

        :return: ``True`` if destroyed successfully, otherwise ``False``
        :rtype: ``bool``
        """
        result = self._sync_request(
            endpoint="objects/networks/{}".format(network.id), method="DELETE"
        )
        return result.status == 204

    def delete_image(self, node_image):
        """
        Destroy an image.

        :param node_image: Node image object.
        :type node_image: :class:`.NodeImage`

        :return: True if the destroy was successful, otherwise False
        :rtype: ``bool``

        """
        result = self._sync_request(
            endpoint="objects/templates/{}".format(node_image.id), method="DELETE"
        )
        return result.status == 204

    def ex_rename_node(self, node, name):
        """
        Modify node name.

        :param name: New node name.
        :type name: ``str``

        :param node: Node
        :type node: :class:`.Node`

        :return: ``True`` or ``False``
        :rtype: ``bool``
        """
        result = self._sync_request(
            data={"name": name},
            endpoint="objects/servers/{}".format(node.id),
            method="PATCH",
        )
        return result.status == 204

    def ex_rename_volume(self, volume, name):
        """
        Modify storage volume name

        :param volume: Storage.
        :type volume: :class:.`StorageVolume`

        :param name: New storage name.
        :type name: ``str``

        :return: ``True`` or ``False``
        :rtype: ``bool``
        """
        result = self._sync_request(
            data={"name": name},
            endpoint="objects/storages/{}".format(volume.id),
            method="PATCH",
        )
        return result.status == 204

    def ex_rename_network(self, network, name):
        """
        Modify networks name.

        :param network: Network.
        :type network: :class:`.GridscaleNetwork`

        :param name: New network name.
        :type name: ``str``

        :return: ``True`` or ``False``
        :rtype: ``bool``
        """
        result = self._sync_request(
            data={"name": name},
            endpoint="objects/networks/{}".format(network.id),
            method="PATCH",
        )
        return result.status == 204

    def reboot_node(self, node, ex_sleep_interval=3):
        """
        Reboot a node.

        :param node: Node object.
        :type node: :class:`.Node`

        :return: True if the reboot was successful, otherwise False.
        :rtype: ``bool``

        :keyword ex_sleep_interval: time to let the shutdown process finish
        :type ex_sleep_interval: ``int``

        """

        if node.extra["power"] is True:
            data = dict({"power": False})
            self._sync_request(
                data=data,
                endpoint="objects/servers/{}/power".format(node.id),
                method="PATCH",
            )
            time.sleep(ex_sleep_interval)

            data = dict({"power": True})
            self._sync_request(
                data=data,
                endpoint="objects/servers/{}/power".format(node.id),
                method="PATCH",
            )
            return True

        else:

            return False

    def import_key_pair_from_string(self, name, key_material):
        data = {"name": name, "sshkey": key_material}
        result = self._sync_request(endpoint="objects/sshkeys/", method="POST", data=data)
        key = self._to_key(result.object, name=name, sshkey=key_material)
        return key

    def list_key_pairs(self):
        """
        List all the available key pair objects.

        :rtype: ``list``of :class:`.KeyPair` objects
        """
        keys = []
        result = self._sync_request(endpoint="objects/sshkeys/")
        for key, value in self._get_response_dict(result).items():
            key = self._to_key(value)
            keys.append(key)
        return keys

    def get_image(self, image_id):
        """
        Get an image based on an image_id.

        :param image_id: Image identifier.
        :type image_id: ``str``

        :return: A NodeImage object.
        :rtype: :class:`.NodeImage`
        """

        response_dict = self._get_response_dict(
            self._sync_request(endpoint="/objects/templates/{}".format(image_id))
        )

        return self._to_node_image(response_dict)

    def start_node(self, node):
        result = self._sync_request(
            data={"power": True},
            endpoint="objects/servers/{}/power".format(node.id),
            method="PATCH",
        )

        return result.status == 204

    def ex_start_node(self, node):
        # NOTE: This method is here for backward compatibility reasons after
        # this method was promoted to be part of the standard compute API in
        # Libcloud v2.7.0
        return self.start_node(node=node)

    def ex_link_isoimage_to_node(self, node, isoimage):
        """
        link and isoimage to a node

        :param node: Node you want to link the iso image to
        :type node: ``object``

        :param isoimage: isomiage you want to link
        :type isoimage: ``object``

        :return: None -> success
        :rtype: ``None``
        """
        result = self._sync_request(
            data={"object_uuid": isoimage.id},
            endpoint="objects/servers/{}/isoimages/".format(node.id),
            method="POST",
        )
        return result

    def attach_volume(self, node, volume):
        """
         Attaches volume to node.

        :param node: Node to attach volume to.
        :type node: :class:`.Node`

        :param volume: Volume to attach.
        :type volume: :class:`.StorageVolume`

        :rytpe: ``bool``
        """
        result = self._sync_request(
            data={"object_uuid": volume.id},
            endpoint="objects/servers/{}/storages/".format(node.id),
            method="POST",
        )
        return result.status == 204

    def ex_link_network_to_node(self, node, network):
        """
        Link a network to a node.

        :param node: Node object to link networks to.
        :type node: :class:`.Node`

        :param network: Network you want to link.
        :type network: :class:`.GridscaleNetwork`

        :return: ``True`` if linked sucessfully, otherwise ``False``
        :rtype: ``bool``
        """
        result = self._sync_request(
            data={"object_uuid": network.id},
            endpoint="objects/servers/{}/networks/".format(node.id),
            method="POST",
        )
        return result.status == 204

    def ex_link_ip_to_node(self, node, ip):
        """
        links a existing ip with a node

        :param node: node object
        :type node: ``object``

        :param ip: ip object
        :type ip: ``object``

        :return: Request ID
        :rtype: ``str``
        """
        result = self._sync_request(
            data={"object_uuid": ip.id},
            endpoint="objects/servers/{}/ips/".format(node.id),
            method="POST",
        )
        return result

    def ex_unlink_isoimage_from_node(self, node, isoimage):
        """
        unlink isoimages from server

        :param node: node you want to unlink the image from
        :type node: ``object``

        :param isoimage: isoimage you want to unlink
        :type isoimage: ``object``

        :return: None -> success
        :rtype: ``None``
        """
        result = self._sync_request(
            endpoint="objects/servers/{}/isoimages/{}".format(node.id, isoimage.id),
            method="DELETE",
        )
        return result

    def ex_unlink_ip_from_node(self, node, ip):
        """
        unlink ips from server

        :param node: node you want to unlink the ip from
        :type node: ``object``

        :param ip: the ip you want to unlink
        :type ip: ``object``

        :return: None -> success
        :rtype: ``None``
        """
        result = self._sync_request(
            endpoint="objects/servers/{}/ips/{}".format(node.id, ip.id), method="DELETE"
        )
        return result

    def ex_unlink_network_from_node(self, node, network):
        """
        Unlink network from node.

        :param node: Node you want to unlink from network.
        :type node: :class:`.Node`

        :param network: Network you want to unlink.
        :type network: :class:`.GridscaleNetwork

        :return: ``True`` if unlink was successful, otherwise ``False``
        :rtype: ``bool``
        """
        result = self._sync_request(
            endpoint="objects/servers/{}/networks/{}".format(node.id, network.id),
            method="DELETE",
        )
        return result.status == 204

    def detach_volume(self, volume):
        """
        Detaches a volume from a node.

        :param volume: Volume to be detached
        :type volume: :class:`.StorageVolume`

        :rtype: ``bool``
        """
        node = volume.extra["relations"]["servers"][0]
        result = self._sync_request(
            endpoint="objects/servers/{}/storages/{}".format(node["object_uuid"], volume.id),
            method="DELETE",
        )
        return result.status == 204

    def ex_storage_rollback(self, volume, snapshot, rollback):
        """
        initiate a rollback on your storage

        :param volume: storage uuid
        :type volume: ``string``

        :param snapshot: snapshot uuid
        :type snapshot: ``string``

        :param rollback: variable
        :type rollback: ``bool``

        :return: RequestID
        :rtype: ``str``
        """
        result = self._sync_request(
            data={"rollback": rollback},
            endpoint="objects/storages/{}/snapshots/" "{}/rollback".format(volume.id, snapshot.id),
            method="PATCH",
        )
        return result

    def ex_list_volumes_for_node(self, node):
        """
        Return a list of associated volumes for the provided node.

        :rtype: ``list`` of :class:`StorageVolume`
        """
        volumes = self.list_volumes()

        result = []
        for volume in volumes:
            related_servers = volume.extra.get("relations", {}).get("servers", [])
            for server in related_servers:
                if server["object_uuid"] == node.id:
                    result.append(volume)

        return result

    def ex_list_ips_for_node(self, node):
        """
        Return a list of associated IPs for the provided node.

        :rype: ``list`` of :class:`GridscaleIp`
        """
        ips = self.ex_list_ips()

        result = []
        for ip in ips:
            related_servers = ip.extra.get("relations", {}).get("servers", [])
            for server in related_servers:
                # TODO: This is not consistent with volumes where key is
                # called "object_uuid"
                if server["server_uuid"] == node.id:
                    result.append(ip)

        return result

    def _to_node(self, data):
        extra_keys = ["cores", "power", "memory", "current_price", "relations"]

        extra = self._extract_values_to_dict(data=data, keys=extra_keys)
        ips = []

        for diction in data["relations"]["public_ips"]:
            ips.append(diction["ip"])

        state = ""

        if data["power"] is True:
            state = NodeState.RUNNING
        else:
            state = NodeState.STOPPED

        node = Node(
            id=data["object_uuid"],
            name=data["name"],
            state=state,
            public_ips=ips,
            created_at=parse_date(data["create_time"]),
            private_ips=None,
            driver=self.connection.driver,
            extra=extra,
        )

        return node

    def _to_volume(self, data):
        extra_keys = ["create_time", "current_price", "storage_type", "relations"]

        extra = self._extract_values_to_dict(data=data, keys=extra_keys)

        storage = StorageVolume(
            id=data["object_uuid"],
            name=data["name"],
            size=data["capacity"],
            driver=self.connection.driver,
            extra=extra,
        )

        return storage

    def _to_volume_snapshot(self, data):
        extra_keys = [
            "labels",
            "status",
            "usage_in_minutes",
            "location_country",
            "current_price",
            "parent_uuid",
        ]

        extra = self._extract_values_to_dict(data=data, keys=extra_keys)

        volume_snapshot = VolumeSnapshot(
            id=data["object_uuid"],
            driver=self.connection.driver,
            size=data["capacity"],
            extra=extra,
            created=parse_date(data["create_time"]),
            state=StorageVolumeState.AVAILABLE,
            name=data["name"],
        )

        return volume_snapshot

    def _to_location(self, data):
        location = NodeLocation(
            id=data["object_uuid"],
            name=data["name"],
            country=data["country"],
            driver=self.connection.driver,
        )
        return location

    def _to_ip(self, data):
        extra_keys = [
            "create_time",
            "current_price",
            "name",
            "relations",
            "reverse_dns",
            "status",
        ]
        extra = self._extract_values_to_dict(data=data, keys=extra_keys)

        ip = GridscaleIp(
            id=data["object_uuid"],
            family=data["family"],
            prefix=data["prefix"],
            create_time=data["create_time"],
            address=data["ip"],
            extra=extra,
        )

        return ip

    def _to_network(self, data):
        network = GridscaleNetwork(
            id=data["object_uuid"],
            name=data["name"],
            create_time=data["create_time"],
            status=data["status"],
            relations=data["relations"],
        )
        return network

    def _to_node_image(self, data):
        extra_keys = [
            "capacity",
            "create_time",
            "labels",
            "ostype",
            "location_name",
            "private",
            "status",
            "usage_in_minutes",
            "version",
        ]

        extra = self._extract_values_to_dict(data=data, keys=extra_keys)

        template = NodeImage(
            id=data["object_uuid"],
            name=data["name"],
            driver=self.connection.driver,
            extra=extra,
        )

        return template

    def _to_key(self, data, name=None, sshkey=None):
        extra = {"uuid": data["object_uuid"], "labels": data.get("labels", [])}

        name = data.get("name", name)
        sshkey = data.get("sshkey", sshkey)

        key = KeyPair(
            name=name,
            fingerprint=data["object_uuid"],
            public_key=sshkey,
            private_key=None,
            extra=extra,
            driver=self.connection.driver,
        )

        return key

    def _extract_values_to_dict(self, data, keys):
        """
        Extract extra values to dict.

        :param data: dict to extract values from.
        :type data: ``dict``
        :param keys: keys to extract
        :type keys: ``List``
        :return: dictionary containing extra values
        :rtype: ``dict``
        """

        result = {}

        for key in keys:
            if key == "memory":
                result[key] = data[key] * 1024
            else:
                result[key] = data[key]

        return result

    def _get_response_dict(self, raw_response):
        """

        Get the actual response dictionary.

        :param raw_response: Nested dictionary.
        :type raw_response: ``dict``

        :return: Not-nested dictionary.
        :rtype: ``dict``
        """
        return list(raw_response.object.values())[0]

    def _get_resource(self, endpoint_suffix, object_uuid):
        """
        Get specific uuid specific resource.

        :param endpoint_suffix: Endpoint resource e.g. servers/.
        :type endpoint_suffix: ``str``
        :param object_uuid: Uuid of resource to be pulled.
        :type object_uuid: ``str``
        :return: Response dictionary.
        :rtype: nested ``dict``
        """
        data = self._sync_request(endpoint="objects/{}/{}".format(endpoint_suffix, object_uuid))

        data = self._get_response_dict(data)

        return data
