
# 
#   Copyright 2016 RIFT.IO Inc
#
#   Licensed 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 logging
import os
import shutil
import tempfile

import requests

from gi import require_version
require_version('RwCal', '1.0')

from gi.repository import (
    GObject,
    RwCal,
    RwTypes,
    RwcalYang,
    )

import rw_status
import rift.cal.rwcal_status as rwcal_status
import rwlogger

logger = logging.getLogger('rwcal.cloudsimproxy')


rwstatus_exception_map = { IndexError: RwTypes.RwStatus.NOTFOUND,
                           KeyError: RwTypes.RwStatus.NOTFOUND,
                           NotImplementedError: RwTypes.RwStatus.NOT_IMPLEMENTED,}

rwstatus = rw_status.rwstatus_from_exc_map(rwstatus_exception_map)
rwcalstatus = rwcal_status.rwcalstatus_from_exc_map(rwstatus_exception_map)


class CloudsimProxyError(Exception):
    pass


class CloudSimProxyPlugin(GObject.Object, RwCal.Cloud):
    DEFAULT_PROXY_HOST = "localhost"
    DEFAULT_PROXY_PORT = 9002

    def __init__(self):
        self._session = None
        self._host = None
        self._port = CloudSimProxyPlugin.DEFAULT_PROXY_PORT

    @property
    def session(self):
        if self._session is None:
            self._session = requests.Session()

        return self._session

    @property
    def host(self):
        return self._host

    @host.setter
    def host(self, host):
        if self._host is not None:
            if host != self._host:
                raise CloudsimProxyError("Cloudsim host changed during execution")

        self._host = host

    def _set_host_from_account(self, account):
        self.host = account.cloudsim_proxy.host

    def _proxy_rpc_call(self, api, **kwargs):
        url = "http://{host}:{port}/api/{api}".format(
                host=self._host,
                port=self._port,
                api=api,
                )

        post_dict = {}
        for key, val in kwargs.items():
            post_dict[key] = val

        logger.debug("Sending post to url %s with json data: %s", url, post_dict)
        r = self.session.post(url, json=post_dict)
        r.raise_for_status()

        response_dict = r.json()
        logger.debug("Got json response: %s", response_dict)

        return_vals = []
        for return_val in response_dict["return_vals"]:
            value = return_val["value"]
            proto_type = return_val["proto_type"]
            if proto_type is not None:
                gi_cls = getattr(RwcalYang, proto_type)
                logger.debug("Deserializing into %s", proto_type)
                gi_obj = gi_cls.from_dict(value)
                value = gi_obj

            return_vals.append(value)

        logger.debug("Returning RPC return values: %s", return_vals)

        if len(return_vals) == 0:
            return None

        elif len(return_vals) == 1:
            return return_vals[0]

        else:
            return tuple(return_vals[1:])

    @rwstatus
    def do_init(self, rwlog_ctx):
        logger.addHandler(
            rwlogger.RwLogger(
                category="rw-cal-log",
                subcategory="cloudsimproxy",
                log_hdl=rwlog_ctx,
            )
        )

    @rwstatus(ret_on_failure=[None])
    def do_get_management_network(self, account):
        """Returns the management network

        Arguments:
            account - a cloud account

        Returns:
            a NetworkInfo object

        """

        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_management_network")

    @rwstatus
    def do_create_tenant(self, account, name):
        """
        Create a new tenant.

        @param name     - name to assign to the tenant.
        """
        raise NotImplementedError()

    @rwstatus
    def do_delete_tenant(self, account, tenant_id):
        """
        delete a tenant.

        @param tenant_id     - id of tenant to be deleted.
        """
        raise NotImplementedError()

    @rwstatus(ret_on_failure=[[]])
    def do_get_tenant_list(self, account):
        """
        List tenants.

        """
        raise NotImplementedError()

    @rwstatus
    def do_create_role(self, account, name):
        """
        Create a new role.

        @param name         - name to assign to the role.
        """
        raise NotImplementedError()

    @rwstatus
    def do_delete_role(self, account, role_id):
        """
        delete a role.

        @param role_id     - id of role to be deleted.
        """
        raise NotImplementedError()

    @rwstatus(ret_on_failure=[[]])
    def do_get_role_list(self, account):
        """
        List roles.

        """
        raise NotImplementedError()

    @rwstatus(ret_on_failure=[None])
    def do_create_image(self, account, image):
        """Create a new image

        Creates a new container based upon the template and tarfile specified.
        Only one image is currently supported for a given instance of the CAL.

        Arguments:
            account - a cloud account
            image   - an ImageInfo object

        Raises:
            An RWErrorDuplicate is raised if create_image is called and there
            is already an image.

        Returns:
            The UUID of the new image

        """
        self._set_host_from_account(account)

        if image.has_field("fileno"):
            logger.debug("Got fileno for cloudsim image create")
            new_fileno = os.dup(image.fileno)
            read_hdl = os.fdopen(new_fileno, 'rb')
            write_hdl = tempfile.NamedTemporaryFile()
            image.location = write_hdl.name
            logger.debug("Created temporary file to store the cloudsim image: %s", image.location)
            shutil.copyfileobj(read_hdl, write_hdl)

            image_dict = image.as_dict()
            del image_dict["fileno"]
        else:
            image_dict = image.as_dict()

        return self._proxy_rpc_call("create_image", image=image_dict)

    @rwstatus
    def do_delete_image(self, account, image_id):
        """Deletes an image

        This function will remove the record of the image from the CAL and
        destroy the associated container.

        Arguments:
            account  - a cloud account
            image_id - the UUID of the image to delete

        Raises:
            An RWErrorNotEmpty exception is raised if there are VMs based on
            this image (the VMs need to be deleted first). An RWErrorNotFound
            is raised if the image_id does not match any of the known images.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_image")

    @rwstatus(ret_on_failure=[None])
    def do_get_image(self, account, image_id):
        """Returns the specified image

        Arguments:
            account  - a cloud account
            image_id - the UUID of the image to retrieve

        Raises:
            An RWErrorNotFound exception is raised if the image_id does not
            match any of the known images.

        Returns:
            An image object

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_image", image_id=image_id)

    @rwstatus(ret_on_failure=[[]])
    def do_get_image_list(self, account):
        """Returns a list of images"""
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_image_list")

    @rwstatus
    def do_create_vm(self, account, vm):
        """Create a VM

        Arguments:
            vm - the VM info used to define the desire VM

        Raises:
            An RWErrorFailure is raised if there is not

        Returns:
            a string containing the unique id of the created VM

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_vm", vm=vm.as_dict())

    @rwstatus
    def do_start_vm(self, account, vm_id):
        """Starts the specified VM

        Arguments:
            vm_id - the id of the vm to start

        Raises:
            An RWErrorNotFound is raised if the specified vm id is not known to
            this driver.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("start_vm", vm_id=vm_id)

    @rwstatus
    def do_stop_vm(self, account, vm_id):
        """Stops the specified VM

        Arguments:
            vm_id - the id of the vm to stop

        Raises:
            An RWErrorNotFound is raised if the specified vm id is not known to
            this driver.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("stop_vm", vm_id=vm_id)

    @rwstatus
    def do_delete_vm(self, account, vm_id):
        """Deletes the specified VM

        Arguments:
            vm_id - the id of the vm to delete

        Raises:
            An RWErrorNotFound is raised if the specified vm id is not known to
            this driver.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_vm", vm_id=vm_id)

    @rwstatus
    def do_reboot_vm(self, account, vm_id):
        """
        reboot a virtual machine.

        @param vm_id     - Instance id of VM to be deleted.
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("reboot_vm", vm_id=vm_id)

    @rwstatus
    def do_get_vm(self, account, vm_id):
        """Returns the specified VM

        Arguments:
            vm_id - the id of the vm to return

        Raises:
            An RWErrorNotFound is raised if the specified vm id is not known to
            this driver.

        Returns:
            a VMInfoItem object

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_vm", vm_id=vm_id)

    @rwstatus(ret_on_failure=[[]])
    def do_get_vm_list(self, account):
        """Returns the a list of the VMs known to the driver

        Returns:
            a list of VMInfoItem objects

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_vm_list")

    @rwstatus
    def do_create_flavor(self, account, flavor):
        """
        create new flavor.

        @param flavor   - Flavor object
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_flavor", flavor=flavor.as_dict())

    @rwstatus
    def do_delete_flavor(self, account, flavor_id):
        """
        Delete flavor.

        @param flavor_id     - Flavor id to be deleted.
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_flavor", flavor_id=flavor_id)

    @rwstatus(ret_on_failure=[None])
    def do_get_flavor(self, account, flavor_id):
        """
        Return the specified flavor

        @param flavor_id - the id of the flavor to return
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_flavor", flavor_id=flavor_id)

    @rwstatus(ret_on_failure=[[]])
    def do_get_flavor_list(self, account):
        """
        Return a list of flavors
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_flavor_list")

    @rwstatus
    def do_add_host(self, account, host):
        raise NotImplementedError()

    @rwstatus
    def do_remove_host(self, account, host_id):
        raise NotImplementedError()

    @rwstatus(ret_on_failure=[None])
    def do_get_host(self, account, host_id):
        raise NotImplementedError()

    @rwstatus(ret_on_failure=[[]])
    def do_get_host_list(self, account):
        raise NotImplementedError()

    @rwstatus
    def do_create_port(self, account, port):
        """Create a port between a network and a virtual machine

        Arguments:
            account - a cloud account
            port    - a description of port to create

        Raises:
            Raises an RWErrorNotFound exception if either the network or the VM
            associated with the port cannot be found.

        Returns:
            the ID of the newly created port.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_port", port=port.as_dict())

    @rwstatus
    def do_delete_port(self, account, port_id):
        """Delete the specified port

        Arguments:
            account - a cloud account
            port_id - the ID of the port to delete

        Raises:
            A RWErrorNotFound exception is raised if the specified port cannot
            be found.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_port", port_id=port_id)

    @rwstatus(ret_on_failure=[None])
    def do_get_port(self, account, port_id):
        """Return the specified port

        Arguments:
            account - a cloud account
            port_id - the ID of the port to return

        Raises:
            A RWErrorNotFound exception is raised if the specified port cannot
            be found.

        Returns:
            The specified port.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_port", port_id=port_id)

    @rwstatus(ret_on_failure=[[]])
    def do_get_port_list(self, account):
        """Returns a list of ports"""

        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_port_list")

    @rwstatus
    def do_create_network(self, account, network):
        """Create a network

        Arguments:
            account - a cloud account
            network - a description of the network to create

        Returns:
            The ID of the newly created network

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_network", network=network.as_dict())

    @rwstatus
    def do_delete_network(self, account, network_id):
        """
        Arguments:
            account    - a cloud account
            network_id - the UUID of the network to delete

        Raises:
            An RWErrorNotFound is raised if the specified network cannot be
            found.

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_network", network_id=network_id)

    @rwstatus(ret_on_failure=[None])
    def do_get_network(self, account, network_id):
        """Returns the specified network

        Arguments:
            account    - a cloud account
            network_id - the UUID of the network to delete

        Raises:
            An RWErrorNotFound is raised if the specified network cannot be
            found.

        Returns:
            The specified network

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_network", network_id=network_id)

    @rwstatus(ret_on_failure=[[]])
    def do_get_network_list(self, account):
        """Returns a list of network objects"""
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_network_list")

    @rwstatus(ret_on_failure=[None])
    def do_validate_cloud_creds(self, account):
        """
        Validates the cloud account credentials for the specified account.
        If creds are not valid, returns an error code & reason string
        Arguments:
            account - a cloud account to validate

        Returns:
            Validation Code and Details String
        """
        self._set_host_from_account(account)

        status = RwcalYang.CloudConnectionStatus()
        try:
            self._proxy_rpc_call("get_vm_list")
        except Exception as e:
            status.status = "failure"
            status.details = "connection to cloudsim server failed: %s" % str(e)
        else:
            status.status = "success"
            status.details = "Connection was successful"

        return status

    @rwstatus(ret_on_failure=[""])
    def do_create_virtual_link(self, account, link_params):
        """Create a new virtual link

        Arguments:
            account     - a cloud account
            link_params - information that defines the type of VDU to create

        Returns:
            The vdu_id
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_virtual_link", link_params=link_params.as_dict())

    @rwstatus(ret_on_failure=[None])
    def do_get_virtual_link(self, account, link_id):
        """Get information about virtual link.

        Arguments:
            account  - a cloud account
            link_id  - id for the virtual-link

        Returns:
            Object of type RwcalYang.VirtualLinkInfoParams
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_virtual_link", link_id=link_id)

    @rwstatus(ret_on_failure=[[]])
    def do_get_virtual_link_list(self, account):
        """Returns the a list of the Virtual links

        Returns:
            a list of RwcalYang.VirtualLinkInfoParams objects

        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_virtual_link_list")

    @rwstatus(ret_on_failure=[None])
    def do_delete_virtual_link(self, account, link_id):
        """Delete the virtual link

        Arguments:
            account  - a cloud account
            link_id  - id for the virtual-link
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_virtual_link", link_id=link_id)

    @rwcalstatus(ret_on_failure=[""])
    def do_create_virtual_link(self, account, link_params):
        """Create a new virtual link

        Arguments:
            account     - a cloud account
            link_params - information that defines the type of VDU to create

        Returns:
            The vdu_id
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_virtual_link", link_params=link_params.as_dict())

    @rwcalstatus(ret_on_failure=[""])
    def do_create_vdu(self, account, vdu_init):
        """Create a new virtual deployment unit

        Arguments:
            account     - a cloud account
            vdu_init  - information about VDU to create (RwcalYang.VDUInitParams)

        Returns:
            The vdu_id
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("create_vdu", vdu_params=vdu_init.as_dict())

    @rwstatus
    def do_modify_vdu(self, account, vdu_modify):
        """Modify Properties of existing virtual deployment unit

        Arguments:
            account     -  a cloud account
            vdu_modify  -  Information about VDU Modification (RwcalYang.VDUModifyParams)
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("modify_vdu", vdu_params=vdu_modify.as_dict())

    @rwstatus
    def do_delete_vdu(self, account, vdu_id):
        """Delete a virtual deployment unit

        Arguments:
            account - a cloud account
            vdu_id  - id for the vdu to be deleted

        Returns:
            None
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("delete_vdu", vdu_id=vdu_id)

    @rwstatus(ret_on_failure=[None])
    def do_get_vdu(self, account, vdu_id):
        """Get information about a virtual deployment unit.

        Arguments:
            account - a cloud account
            vdu_id  - id for the vdu

        Returns:
            Object of type RwcalYang.VDUInfoParams
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_vdu", vdu_id=vdu_id)

    @rwstatus(ret_on_failure=[None])
    def do_get_vdu_list(self, account):
        """Get information about all the virtual deployment units

        Arguments:
            account     - a cloud account

        Returns:
            A list of objects of type RwcalYang.VDUInfoParams
        """
        self._set_host_from_account(account)
        return self._proxy_rpc_call("get_vdu_list")
