# 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.common.gridscale import GridscaleBaseDriver
from libcloud.common.gridscale import GridscaleConnection
from libcloud.compute.base import NodeImage, NodeLocation, VolumeSnapshot, \
    Node, StorageVolume, KeyPair, NodeState, StorageVolumeState, NodeDriver
from libcloud.compute.providers import Provider
from libcloud.utils.iso8601 import parse_date


class GridscaleIp(object):
    """
    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(object):
    """
    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(GridscaleNodeDriver, self).__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
