#!/usr/bin/python

# 
#   Copyright 2017 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
from novaclient import client as nvclient


class NovaDrvAPIVersionException(Exception):
    def __init__(self, errors):
        self.errors = errors
        super(NovaDrvAPIVersionException, self).__init__("Multiple Exception Received")
        
    def __str__(self):
        return self.__repr__()
        
    def __repr__(self):
        msg = "{} : Following Exception(s) have occured during Nova API discovery".format(self.__class__)
        for n,e in enumerate(self.errors):
            msg += "\n"
            msg += " {}:  {}".format(n, str(e))
        return msg


class NovaDriver(object):
    """
    NovaDriver Class for compute orchestration
    """
    ### List of supported API versions in prioritized order 
    supported_versions = ["2.1", "2.0"]
    
    def __init__(self,
                 sess_handle,
                 region_name    = 'RegionOne',
                 service_type   = 'compute',
                 logger = None):
        """
        Constructor for NovaDriver class
        Arguments:
        sess_handle (instance of class SessionDriver)
        region_name (string): Region Name
        service_type(string): Type of service in service catalog
        logger (instance of logging.Logger)
        """

        if logger is None:
            self.log = logging.getLogger('rwcal.openstack.nova')
            self.log.setLevel(logging.DEBUG)
        else:
            self.log = logger
        
        self._sess_handle = sess_handle

        #### Attempt to use API versions in prioritized order defined in
        #### NovaDriver.supported_versions
        def select_version(version):
            try:
                self.log.info("Attempting to use Nova v%s APIs", version)
                nvdrv = nvclient.Client(version=version,
                                        region_name = region_name,
                                        service_type = service_type,
                                        session = self._sess_handle.session,
                                        logger = self.log)
            except Exception as e:
                self.log.info(str(e))
                raise
            else:
                self.log.info("Nova API v%s selected", version)
                return (version, nvdrv)

        errors = []
        for v in NovaDriver.supported_versions:
            try:
                (self._version, self._nv_drv) = select_version(v)
            except Exception as e:
                errors.append(e)
            else:
                break
        else:
            raise NovaDrvAPIVersionException(errors)

    @property
    def project_id(self):
        return self._sess_handle.project_id

    @property
    def nova_endpoint(self):
        return self._nv_drv.client.get_endpoint()

    @property
    def nova_quota(self):
        """
        Returns Nova Quota (a dictionary) for project
        """
        try:
            quota = self._nv_drv.quotas.get(self.project_id)
        except Exception as e:
            self.log.exception("Get Nova quota operation failed. Exception: %s", str(e))
            raise
        return quota.to_dict()
    
    def extensions_list(self):
        """
        Returns a list of available nova extensions.
        Arguments:
           None
        Returns:
           A list of dictionaries. Each dictionary contains attributes for a single NOVA extension
        """
        try:
            extensions = self._nv_drv.list_extensions.show_all()
        except Exception as e:
            self.log.exception("List extension operation failed. Exception: %s", str(e))
            raise
        return [ ext.to_dict() for ext in extensions ]
            
    
    def _get_nova_connection(self):
        """
        Returns instance of object novaclient.client.Client
        Use for DEBUG ONLY
        """
        return self._nv_drv

    def _flavor_extra_spec_get(self, flavor):
        """
        Get extra_specs associated with a flavor
        Arguments:
           flavor: Object of novaclient.v2.flavors.Flavor

        Returns:
           A dictionary of extra_specs (key-value pairs)
        """
        try:
            extra_specs = flavor.get_keys()
        except Exception as e:
            self.log.exception("Could not get the EPA attributes for flavor with flavor_id : %s. Exception: %s",
                               flavor.id, str(e))
            raise
        return extra_specs
    
    def _flavor_get(self, flavor_id):
        """
        Get flavor by flavor_id
        Arguments:
           flavor_id(string): UUID of flavor_id

        Returns:
        dictionary of flavor parameters
        """
        try:
            flavor = self._nv_drv.flavors.get(flavor_id)
        except Exception as e:
            self.log.exception("Did not find flavor with flavor_id : %s. Exception: %s",flavor_id, str(e))
            raise
        response = flavor.to_dict()
        response['extra_specs'] = self._flavor_extra_spec_get(flavor)
        return response
        
        try:
            extra_specs = flavor.get_keys()
        except Exception as e:
            self.log.exception("Could not get the EPA attributes for flavor with flavor_id : %s. Exception: %s",
                               flavor_id, str(e))
            raise

        response = flavor.to_dict()
        assert 'extra_specs' not in response, "Key extra_specs present as flavor attribute"
        response['extra_specs'] = extra_specs
        return response

    def flavor_get(self, flavor_id):
        """
        Get flavor by flavor_id
        Arguments:
           flavor_id(string): UUID of flavor_id

        Returns:
        dictionary of flavor parameters
        """
        return self._flavor_get(flavor_id)

    def flavor_find(self, **kwargs):
        """
        Returns list of all flavors (dictionary) matching the filters provided in kwargs 

        Arguments:
          A dictionary in following keys
             {
                "vcpus": Number of vcpus required
                "ram"  : Memory in MB
                "disk" : Secondary storage in GB
             }
        Returns:
           A list of dictionaries. Each dictionary contains attributes for a single flavor instance
        """
        try:
            flavor_list = self._nv_drv.flavors.findall(**kwargs)
        except Exception as e:
            self.log.exception("Find Flavor operation failed. Exception: %s",str(e))
            raise
        
        flavor_info = list()
        for f in flavor_list:
            flavor = f.to_dict()
            flavor['extra_specs'] = self._flavor_extra_spec_get(f)
            flavor_info.append(flavor)
            
        return flavor_info
    
    def flavor_list(self):
        """
        Returns list of all flavors (dictionary per flavor)

        Arguments:
           None
        Returns:
           A list of dictionaries. Each dictionary contains attributes for a single flavor instance
        """
        flavors = []
        flavor_info = []
        
        try:
            flavors = self._nv_drv.flavors.list()
        except Exception as e:
            self.log.exception("List Flavor operation failed. Exception: %s",str(e))
            raise
        if flavors:
            flavor_info = [ self.flavor_get(flv.id) for flv in flavors ]
        return flavor_info

    def flavor_create(self, name, ram, vcpu, disk, extra_specs):
        """
        Create a new flavor

        Arguments:
           name   (string):  Name of the new flavor
           ram    (int)   :  Memory in MB
           vcpus  (int)   :  Number of VCPUs
           disk   (int)   :  Secondary storage size in GB
           extra_specs (dictionary): EPA attributes dictionary

        Returns:
           flavor_id (string): UUID of flavor created
        """
        try:
            flavor = self._nv_drv.flavors.create(name         = name,
                                                 ram          = ram,
                                                 vcpus        = vcpu,
                                                 disk         = disk,
                                                 flavorid     = 'auto',
                                                 ephemeral    = 0,
                                                 swap         = 0,
                                                 rxtx_factor  = 1.0,
                                                 is_public    = True)
        except Exception as e:
            self.log.exception("Create Flavor operation failed. Exception: %s",str(e))
            raise

        if extra_specs:
            try:
                flavor.set_keys(extra_specs)
            except Exception as e:
                self.log.exception("Set Key operation failed for flavor: %s. Exception: %s",
                                   flavor.id, str(e))
                raise
        return flavor.id

    def flavor_delete(self, flavor_id):
        """
        Deletes a flavor identified by flavor_id

        Arguments:
           flavor_id (string):  UUID of flavor to be deleted

        Returns: None
        """
        assert flavor_id == self._flavor_get(flavor_id)['id']
        try:
            self._nv_drv.flavors.delete(flavor_id)
        except Exception as e:
            self.log.exception("Delete flavor operation failed for flavor: %s. Exception: %s",
                               flavor_id, str(e))
            raise


    def server_list(self):
        """
        Returns a list of available VMs for the project

        Arguments: None

        Returns:
           A list of dictionaries. Each dictionary contains attributes associated
           with individual VM
        """
        servers     = []
        server_info = []
        try:
            servers     = self._nv_drv.servers.list()
        except Exception as e:
            self.log.exception("List Server operation failed. Exception: %s", str(e))
            raise
        server_info = [ server.to_dict() for server in servers]
        return server_info

    def _nova_server_get(self, server_id):
        """
        Returns a dictionary of attributes associated with VM identified by service_id

        Arguments:
          server_id (string): UUID of the VM/server for which information is requested

        Returns:
          A dictionary object with attributes associated with VM identified by server_id
        """
        try:
            server = self._nv_drv.servers.get(server = server_id)
        except Exception as e:
            self.log.exception("Get Server operation failed for server_id: %s. Exception: %s",
                               server_id, str(e))
            raise
        else:
            return server.to_dict()

    def server_get(self, server_id):
        """
        Returns a dictionary of attributes associated with VM identified by service_id

        Arguments:
          server_id (string): UUID of the VM/server for which information is requested

        Returns:
          A dictionary object with attributes associated with VM identified by server_id
        """
        return self._nova_server_get(server_id)

    def server_create(self, **kwargs):
        """
        Creates a new VM/server instance

        Arguments:
          A dictionary of following key-value pairs
         {
           server_name(string)        : Name of the VM/Server
           flavor_id  (string)        : UUID of the flavor to be used for VM
           image_id   (string)        : UUID of the image to be used VM/Server instance,
                                             This could be None if volumes (with images) are being used
           network_list(List)         : A List of network_ids. A port will be created in these networks
           port_list (List)           : A List of port-ids. These ports will be added to VM.
           metadata   (dict)          : A dictionary of arbitrary key-value pairs associated with VM/server
           userdata   (string)        : A script which shall be executed during first boot of the VM
           availability_zone (string) : A name of the availability zone where instance should be launched
           scheduler_hints (string)   : Openstack scheduler_hints to be passed to nova scheduler
         }
        Returns:
          server_id (string): UUID of the VM/server created

        """
        nics = []
        if 'network_list' in kwargs:
            for network_id in kwargs['network_list']:
                nics.append({'net-id': network_id})

        if 'port_list' in kwargs:
            for port_id in kwargs['port_list']:
                nics.append({'port-id': port_id})

        try:
            server = self._nv_drv.servers.create(
                kwargs['name'],
                kwargs['image_id'],
                kwargs['flavor_id'],
                meta                 = kwargs['metadata'] if 'metadata' in kwargs else None,
                files                = kwargs['files'] if 'files' in kwargs else None,
                reservation_id       = None,
                min_count            = None,
                max_count            = None,
                userdata             = kwargs['userdata'] if 'userdata' in kwargs else None,
                security_groups      = kwargs['security_groups'] if 'security_groups' in kwargs else None,
                availability_zone    = kwargs['availability_zone'] if 'availability_zone' in kwargs else None,
                block_device_mapping_v2 = kwargs['block_device_mapping_v2'] if 'block_device_mapping_v2' in kwargs else None,
                nics                 = nics,
                scheduler_hints      = kwargs['scheduler_hints'] if 'scheduler_hints' in kwargs else None,
                config_drive         = kwargs['config_drive'] if 'config_drive' in kwargs else None
            )
            
        except Exception as e:
            self.log.exception("Create Server operation failed. Exception: %s", str(e))
            raise
        return server.to_dict()['id']

    def server_delete(self, server_id):
        """
        Deletes a server identified by server_id

        Arguments:
           server_id (string): UUID of the server to be deleted

        Returns: None
        """
        try:
            self._nv_drv.servers.delete(server_id)
        except Exception as e:
            self.log.exception("Delete server operation failed for server_id: %s. Exception: %s",
                               server_id, str(e))
            raise

    def server_start(self, server_id):
        """
        Starts a server identified by server_id

        Arguments:
           server_id (string): UUID of the server to be started

        Returns: None
        """
        try:
            self._nv_drv.servers.start(server_id)
        except Exception as e:
            self.log.exception("Start Server operation failed for server_id : %s. Exception: %s",
                               server_id, str(e))
            raise

    def server_stop(self, server_id):
        """
        Arguments:
           server_id (string): UUID of the server to be stopped

        Returns: None
        """
        try:
            self._nv_drv.servers.stop(server_id)
        except Exception as e:
            self.log.exception("Stop Server operation failed for server_id : %s. Exception: %s",
                               server_id, str(e))
            raise

    def server_pause(self, server_id):
        """
        Arguments:
           server_id (string): UUID of the server to be paused

        Returns: None
        """
        try:
            self._nv_drv.servers.pause(server_id)
        except Exception as e:
            self.log.exception("Pause Server operation failed for server_id : %s. Exception: %s",
                               server_id, str(e))
            raise

    def server_unpause(self, server_id):
        """
        Arguments:
           server_id (string): UUID of the server to be unpaused

        Returns: None
        """
        try:
            self._nv_drv.servers.unpause(server_id)
        except Exception as e:
            self.log.exception("Resume Server operation failed for server_id : %s. Exception: %s",
                               server_id, str(e))
            raise


    def server_suspend(self, server_id):
        """
        Arguments:
           server_id (string): UUID of the server to be suspended

        Returns: None
        """
        try:
            self._nv_drv.servers.suspend(server_id)
        except Exception as e:
            self.log.exception("Suspend Server operation failed for server_id : %s. Exception: %s",
                               server_id, str(e))


    def server_resume(self, server_id):
        """
        Arguments:
           server_id (string): UUID of the server to be resumed

        Returns: None
        """
        try:
            self._nv_drv.servers.resume(server_id)
        except Exception as e:
            self.log.exception("Resume Server operation failed for server_id : %s. Exception: %s",
                               server_id, str(e))
            raise

    def server_reboot(self, server_id, reboot_type):
        """
        Arguments:
           server_id (string) : UUID of the server to be rebooted
           reboot_type(string):
                         'SOFT': Soft Reboot
                         'HARD': Hard Reboot
        Returns: None
        """
        try:
            self._nv_drv.servers.reboot(server_id, reboot_type)
        except Exception as e:
            self.log.exception("Reboot Server operation failed for server_id: %s. Exception: %s",
                               server_id, str(e))
            raise

    def server_console(self, server_id, console_type = 'novnc'):
        """
        Arguments:
           server_id (string) : UUID of the server to be rebooted
           console_type(string):
                               'novnc',
                               'xvpvnc'
        Returns:
          A dictionary object response for console information
        """
        try:
            console_info = self._nv_drv.servers.get_vnc_console(server_id, console_type)
        except Exception as e:
            self.log.exception("Server Get-Console operation failed for server_id: %s. Exception: %s",
                               server_id, str(e))
            raise
        return console_info

    def server_rebuild(self, server_id, image_id):
        """
        Arguments:
           server_id (string) : UUID of the server to be rebooted
           image_id (string)  : UUID of the image to use
        Returns: None
        """

        try:
            self._nv_drv.servers.rebuild(server_id, image_id)
        except Exception as e:
            self.log.exception("Rebuild Server operation failed for server_id: %s. Exception: %s",
                               server_id, str(e))
            raise


    def server_add_port(self, server_id, port_id):
        """
        Arguments:
           server_id (string): UUID of the server
           port_id   (string): UUID of the port to be attached

        Returns: None
        """
        try:
            self._nv_drv.servers.interface_attach(server_id,
                                            port_id,
                                            net_id = None,
                                            fixed_ip = None)
        except Exception as e:
            self.log.exception("Server Port Add operation failed for server_id : %s, port_id : %s. Exception: %s",
                               server_id, port_id, str(e))
            raise

    def server_delete_port(self, server_id, port_id):
        """
        Arguments:
           server_id (string): UUID of the server
           port_id   (string): UUID of the port to be deleted
        Returns: None

        """
        try:
            self._nv_drv.servers.interface_detach(server_id, port_id)
        except Exception as e:
            self.log.exception("Server Port Delete operation failed for server_id : %s, port_id : %s. Exception: %s",
                               server_id, port_id, str(e))
            raise

    def floating_ip_list(self):
        """
        Arguments:
            None
        Returns:
            List of objects of floating IP nova class (novaclient.v2.floating_ips.FloatingIP)
        """
        try:
            ip_list = self._nv_drv.floating_ips.list()
        except Exception as e:
            self.log.exception("Floating IP List operation failed. Exception: %s", str(e))
            raise

        return ip_list

    def floating_ip_create(self, pool):
        """
        Arguments:
           pool (string): Name of the pool (optional)
        Returns:
           An object of floating IP nova class (novaclient.v2.floating_ips.FloatingIP)
        """
        try:
            floating_ip = self._nv_drv.floating_ips.create(pool)
        except Exception as e:
            self.log.exception("Floating IP Create operation failed. Exception: %s", str(e))
            raise

        return floating_ip

    def floating_ip_delete(self, floating_ip):
        """
        Arguments:
           floating_ip: An object of floating IP nova class (novaclient.v2.floating_ips.FloatingIP)
        Returns:
           None
        """
        try:
            floating_ip = self._nv_drv.floating_ips.delete(floating_ip)
        except Exception as e:
            self.log.exception("Floating IP Delete operation failed. Exception: %s", str(e))
            raise

    def floating_ip_assign(self, server_id, floating_ip, fixed_ip):
        """
        Arguments:
           server_id (string)  : UUID of the server
           floating_ip (string): IP address string for floating-ip
           fixed_ip (string)   : IP address string for the fixed-ip with which floating ip will be associated
        Returns:
           None
        """
        try:
            self._nv_drv.servers.add_floating_ip(server_id, floating_ip, fixed_ip)
        except Exception as e:
            self.log.exception("Assign Floating IP operation failed. Exception: %s", str(e))
            raise

    def floating_ip_release(self, server_id, floating_ip):
        """
        Arguments:
           server_id (string)  : UUID of the server
           floating_ip (string): IP address string for floating-ip
        Returns:
           None
        """
        try:
            self._nv_drv.servers.remove_floating_ip(server_id, floating_ip)
        except Exception as e:
            self.log.exception("Release Floating IP operation failed. Exception: %s", str(e))
            raise

    def volume_list(self, server_id):
        """
          List of volumes attached to the server
  
          Arguments:
              None
          Returns:
             List of dictionary objects where dictionary is representation of class (novaclient.v2.volumes.Volume)
        """
        try:
            volumes = self._nv_drv.volumes.get_server_volumes(server_id=server_id)
        except Exception as e:
            self.log.exception("Get volume information failed. Exception: %s", str(e))
            raise

        volume_info = [v.to_dict() for v in volumes]
        return volume_info


    def group_list(self):
        """
        List of Server Affinity and Anti-Affinity Groups

        Arguments:
            None
        Returns:
           List of dictionary objects where dictionary is representation of class (novaclient.v2.server_groups.ServerGroup)
        """
        try:
            group_list = self._nv_drv.server_groups.list()
        except Exception as e:
            self.log.exception("Server Group List operation failed. Exception: %s", str(e))
            raise

        group_info = [ group.to_dict() for group in group_list ]
        return group_info

        
    def security_group_list(self):
        """
        List of Security Group
        Arguments:
        None
        Returns:
        List of dictionary objects representating novaclient.v2.security_groups.SecurityGroup class
        """
        try:
            sec_groups = self._nv_drv.security_groups.list()
        except Exception as e:
            self.log.exception("Security Group List operation failed. Exception: %s", str(e))
            raise
        sec_info = [ sec_group.to_dict() for sec_group in sec_groups]
        return sec_info
    
