# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2011 Cisco Systems, Inc.  All rights reserved.
#
#    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.
#
# @author: Sumit Naiksatam, Cisco Systems, Inc.

import inspect
import logging

from quantum.common import exceptions as exc
from quantum.openstack.common import importutils
from quantum.plugins.cisco.common import cisco_constants as const
from quantum.plugins.cisco.common import cisco_credentials as cred
from quantum.plugins.cisco.common import cisco_exceptions as cexc
from quantum.plugins.cisco.common import cisco_utils as cutil
from quantum.plugins.cisco.db import api as db
from quantum.plugins.cisco.db import l2network_db as cdb
from quantum.plugins.cisco import l2network_plugin_configuration as conf
from quantum.quantum_plugin_base import QuantumPluginBase


LOG = logging.getLogger(__name__)


class L2Network(QuantumPluginBase):
    """ L2 Network Framework Plugin """
    supported_extension_aliases = ["Cisco Multiport", "Cisco Credential",
                                   "Cisco Port Profile", "Cisco qos",
                                   "Cisco Nova Tenant"]

    def __init__(self):
        cdb.initialize()
        cred.Store.initialize()
        self._model = importutils.import_object(conf.MODEL_CLASS)
        self._vlan_mgr = importutils.import_object(conf.MANAGER_CLASS)
        LOG.debug("L2Network plugin initialization done successfully\n")

    """
    Core API implementation
    """
    def get_all_networks(self, tenant_id, **kwargs):
        """
        Returns a dictionary containing all
        <network_uuid, network_name> for
        the specified tenant.
        """
        LOG.debug("get_all_networks() called\n")
        self._invoke_device_plugins(self._func_name(), [tenant_id])
        networks_list = db.network_list(tenant_id)
        new_networks_list = []
        for network in networks_list:
            new_network_dict = cutil.make_net_dict(network[const.UUID],
                                                   network[const.NETWORKNAME],
                                                   [])
            new_networks_list.append(new_network_dict)

        return new_networks_list

    def create_network(self, tenant_id, net_name, **kwargs):
        """
        Creates a new Virtual Network, and assigns it
        a symbolic name.
        """
        LOG.debug("create_network() called\n")
        new_network = db.network_create(tenant_id, net_name)
        new_net_id = new_network[const.UUID]
        vlan_id = self._get_vlan_for_tenant(tenant_id, net_name)
        vlan_name = self._get_vlan_name(new_net_id, str(vlan_id))
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_name,
                                                        new_net_id, vlan_name,
                                                        vlan_id])
        cdb.add_vlan_binding(vlan_id, vlan_name, new_net_id)
        new_net_dict = {const.NET_ID: new_net_id,
                        const.NET_NAME: net_name,
                        const.NET_PORTS: []}
        return new_net_dict

    def delete_network(self, tenant_id, net_id):
        """
        Deletes the network with the specified network identifier
        belonging to the specified tenant.
        """
        LOG.debug("delete_network() called\n")
        db.validate_network_ownership(tenant_id, net_id)
        net = db.network_get(net_id)
        if net:
            if len(net[const.NETWORKPORTS]) > 0:
                ports_on_net = db.port_list(net_id)
                for port in ports_on_net:
                    if port[const.INTERFACEID]:
                        raise exc.NetworkInUse(net_id=net_id)
                for port in ports_on_net:
                    self.delete_port(tenant_id, net_id, port[const.UUID])

            self._invoke_device_plugins(self._func_name(), [tenant_id, net_id])
            net_dict = cutil.make_net_dict(net[const.UUID],
                                           net[const.NETWORKNAME],
                                           [])
            self._release_vlan_for_tenant(tenant_id, net_id)
            cdb.remove_vlan_binding(net_id)
            db.network_destroy(net_id)
            return net_dict
        # Network not found
        raise exc.NetworkNotFound(net_id=net_id)

    def get_network_details(self, tenant_id, net_id):
        """
        Gets the details of a particular network
        """
        LOG.debug("get_network_details() called\n")
        db.validate_network_ownership(tenant_id, net_id)
        network = db.network_get(net_id)
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id])
        ports_list = network[const.NETWORKPORTS]
        ports_on_net = []
        for port in ports_list:
            new_port = cutil.make_port_dict(port[const.UUID],
                                            port[const.PORTSTATE],
                                            port[const.NETWORKID],
                                            port[const.INTERFACEID])
            ports_on_net.append(new_port)

        new_network = cutil.make_net_dict(network[const.UUID],
                                          network[const.NETWORKNAME],
                                          ports_on_net)

        return new_network

    def update_network(self, tenant_id, net_id, **kwargs):
        """
        Updates the symbolic name belonging to a particular
        Virtual Network.
        """
        LOG.debug("update_network() called\n")
        db.validate_network_ownership(tenant_id, net_id)
        network = db.network_update(net_id, tenant_id, **kwargs)
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
                                                        kwargs])
        net_dict = cutil.make_net_dict(network[const.UUID],
                                       network[const.NETWORKNAME],
                                       [])
        return net_dict

    def get_all_ports(self, tenant_id, net_id, **kwargs):
        """
        Retrieves all port identifiers belonging to the
        specified Virtual Network.
        """
        LOG.debug("get_all_ports() called\n")
        db.validate_network_ownership(tenant_id, net_id)
        network = db.network_get(net_id)
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id])
        ports_list = network[const.NETWORKPORTS]
        ports_on_net = []
        for port in ports_list:
            new_port = cutil.make_port_dict(port[const.UUID],
                                            port[const.PORTSTATE],
                                            port[const.NETWORKID],
                                            port[const.INTERFACEID])
            ports_on_net.append(new_port)

        return ports_on_net

    def create_port(self, tenant_id, net_id, port_state=None, **kwargs):
        """
        Creates a port on the specified Virtual Network.
        """
        LOG.debug("create_port() called\n")

        db.validate_network_ownership(tenant_id, net_id)
        port = db.port_create(net_id, port_state)
        unique_port_id_string = port[const.UUID]
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
                                                        port_state,
                                                        unique_port_id_string])
        new_port_dict = cutil.make_port_dict(port[const.UUID],
                                             port[const.PORTSTATE],
                                             port[const.NETWORKID],
                                             port[const.INTERFACEID])
        return new_port_dict

    def delete_port(self, tenant_id, net_id, port_id):
        """
        Deletes a port on a specified Virtual Network,
        if the port contains a remote interface attachment,
        the remote interface should first be un-plugged and
        then the port can be deleted.
        """
        LOG.debug("delete_port() called\n")
        db.validate_port_ownership(tenant_id, net_id, port_id)
        network = db.network_get(net_id)
        port = db.port_get(net_id, port_id)
        attachment_id = port[const.INTERFACEID]
        if not attachment_id:
            self._invoke_device_plugins(self._func_name(), [tenant_id,
                                                            net_id,
                                                            port_id])
            db.port_destroy(net_id, port_id)
            new_port_dict = cutil.make_port_dict(port_id, None, None, None)
            return new_port_dict
        else:
            raise exc.PortInUse(port_id=port_id, net_id=net_id,
                                att_id=attachment_id)

    def update_port(self, tenant_id, net_id, port_id, **kwargs):
        """
        Updates the state of a port on the specified Virtual Network.
        """
        LOG.debug("update_port() called\n")
        db.validate_port_ownership(tenant_id, net_id, port_id)
        network = db.network_get(net_id)
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
                                                        port_id, kwargs])
        self._validate_port_state(kwargs["state"])
        db.port_update(port_id, net_id, **kwargs)

        new_port_dict = cutil.make_port_dict(port_id, kwargs["state"], net_id,
                                             None)
        return new_port_dict

    def get_port_details(self, tenant_id, net_id, port_id):
        """
        This method allows the user to retrieve a remote interface
        that is attached to this particular port.
        """
        LOG.debug("get_port_details() called\n")
        db.validate_port_ownership(tenant_id, net_id, port_id)
        network = db.network_get(net_id)
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
                                                        port_id])
        port = db.port_get(net_id, port_id)
        new_port_dict = cutil.make_port_dict(port[const.UUID],
                                             port[const.PORTSTATE],
                                             port[const.NETWORKID],
                                             port[const.INTERFACEID])
        return new_port_dict

    def plug_interface(self, tenant_id, net_id, port_id,
                       remote_interface_id):
        """
        Provides connectivity to a remote interface to the
        specified Virtual Network.
        """
        LOG.debug("plug_interface() called\n")
        db.validate_port_ownership(tenant_id, net_id, port_id)
        network = db.network_get(net_id)
        port = db.port_get(net_id, port_id)
        attachment_id = port[const.INTERFACEID]
        if attachment_id is None:
            raise cexc.InvalidAttach(port_id=port_id, net_id=net_id,
                                     att_id=remote_interface_id)
        attachment_id = attachment_id[:const.UUID_LENGTH]
        remote_interface_id = remote_interface_id[:const.UUID_LENGTH]
        if remote_interface_id != attachment_id:
            LOG.debug("Existing attachment_id:%s, remote_interface_id:%s" %
                      (attachment_id, remote_interface_id))
            raise exc.PortInUse(port_id=port_id, net_id=net_id,
                                att_id=attachment_id)
        self._invoke_device_plugins(self._func_name(), [tenant_id,
                                                        net_id, port_id,
                                                        attachment_id])
        db.port_unset_attachment(net_id, port_id)
        db.port_set_attachment(net_id, port_id, attachment_id)
        #Note: The remote_interface_id gets associated with the port
        # when the VM is instantiated. The plug interface call results
        # in putting the port on the VLAN associated with this network

    def unplug_interface(self, tenant_id, net_id, port_id):
        """
        Removes connectivity of a remote interface to the
        specified Virtual Network.
        """
        LOG.debug("unplug_interface() called\n")
        db.validate_port_ownership(tenant_id, net_id, port_id)
        network = db.network_get(net_id)
        port = db.port_get(net_id, port_id)
        attachment_id = port[const.INTERFACEID]
        if attachment_id is None:
            raise cexc.InvalidDetach(port_id=port_id, net_id=net_id,
                                     att_id=remote_interface_id)
        self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
                                                        port_id])
        attachment_id = attachment_id[:const.UUID_LENGTH]
        attachment_id = attachment_id + const.UNPLUGGED
        db.port_unset_attachment(net_id, port_id)
        db.port_set_attachment(net_id, port_id, attachment_id)

    """
    Extension API implementation
    """
    def get_all_portprofiles(self, tenant_id):
        """Get all port profiles"""
        LOG.debug("get_all_portprofiles() called\n")
        pplist = cdb.get_all_portprofiles()
        new_pplist = []
        for portprofile in pplist:
            new_pp = cutil.make_portprofile_dict(tenant_id,
                                                 portprofile[const.UUID],
                                                 portprofile[const.PPNAME],
                                                 portprofile[const.PPQOS])
            new_pplist.append(new_pp)

        return new_pplist

    def get_portprofile_details(self, tenant_id, profile_id):
        """Get port profile details"""
        LOG.debug("get_portprofile_details() called\n")
        try:
            portprofile = cdb.get_portprofile(tenant_id, profile_id)
        except Exception:
            raise cexc.PortProfileNotFound(tenant_id=tenant_id,
                                           portprofile_id=profile_id)

        new_pp = cutil.make_portprofile_dict(tenant_id,
                                             portprofile[const.UUID],
                                             portprofile[const.PPNAME],
                                             portprofile[const.PPQOS])
        return new_pp

    def create_portprofile(self, tenant_id, profile_name, qos):
        """Create port profile"""
        LOG.debug("create_portprofile() called\n")
        portprofile = cdb.add_portprofile(tenant_id, profile_name,
                                          const.NO_VLAN_ID, qos)
        new_pp = cutil.make_portprofile_dict(tenant_id,
                                             portprofile[const.UUID],
                                             portprofile[const.PPNAME],
                                             portprofile[const.PPQOS])
        return new_pp

    def delete_portprofile(self, tenant_id, profile_id):
        """Delete portprofile"""
        LOG.debug("delete_portprofile() called\n")
        try:
            portprofile = cdb.get_portprofile(tenant_id, profile_id)
        except Exception:
            raise cexc.PortProfileNotFound(tenant_id=tenant_id,
                                           portprofile_id=profile_id)

        plist = cdb.get_pp_binding(tenant_id, profile_id)
        if plist:
            raise cexc.PortProfileInvalidDelete(tenant_id=tenant_id,
                                                profile_id=profile_id)
        else:
            cdb.remove_portprofile(tenant_id, profile_id)

    def rename_portprofile(self, tenant_id, profile_id, new_name):
        """Rename port profile"""
        LOG.debug("rename_portprofile() called\n")
        try:
            portprofile = cdb.get_portprofile(tenant_id, profile_id)
        except Exception:
            raise cexc.PortProfileNotFound(tenant_id=tenant_id,
                                           portprofile_id=profile_id)
        portprofile = cdb.update_portprofile(tenant_id, profile_id, new_name)
        new_pp = cutil.make_portprofile_dict(tenant_id,
                                             portprofile[const.UUID],
                                             portprofile[const.PPNAME],
                                             portprofile[const.PPQOS])
        return new_pp

    def associate_portprofile(self, tenant_id, net_id,
                              port_id, portprofile_id):
        """Associate port profile"""
        LOG.debug("associate_portprofile() called\n")
        try:
            portprofile = cdb.get_portprofile(tenant_id, portprofile_id)
        except Exception:
            raise cexc.PortProfileNotFound(tenant_id=tenant_id,
                                           portprofile_id=portprofile_id)

        cdb.add_pp_binding(tenant_id, port_id, portprofile_id, False)

    def disassociate_portprofile(self, tenant_id, net_id,
                                 port_id, portprofile_id):
        """Disassociate port profile"""
        LOG.debug("disassociate_portprofile() called\n")
        try:
            portprofile = cdb.get_portprofile(tenant_id, portprofile_id)
        except Exception:
            raise cexc.PortProfileNotFound(tenant_id=tenant_id,
                                           portprofile_id=portprofile_id)

        cdb.remove_pp_binding(tenant_id, port_id, portprofile_id)

    def get_all_qoss(self, tenant_id):
        """Get all QoS levels"""
        LOG.debug("get_all_qoss() called\n")
        qoslist = cdb.get_all_qoss(tenant_id)
        return qoslist

    def get_qos_details(self, tenant_id, qos_id):
        """Get QoS Details"""
        LOG.debug("get_qos_details() called\n")
        try:
            qos_level = cdb.get_qos(tenant_id, qos_id)
        except Exception:
            raise cexc.QosNotFound(tenant_id=tenant_id,
                                   qos_id=qos_id)
        return qos_level

    def create_qos(self, tenant_id, qos_name, qos_desc):
        """Create a QoS level"""
        LOG.debug("create_qos() called\n")
        qos = cdb.add_qos(tenant_id, qos_name, str(qos_desc))
        return qos

    def delete_qos(self, tenant_id, qos_id):
        """Delete a QoS level"""
        LOG.debug("delete_qos() called\n")
        try:
            qos_level = cdb.get_qos(tenant_id, qos_id)
        except Exception:
            raise cexc.QosNotFound(tenant_id=tenant_id,
                                   qos_id=qos_id)
        return cdb.remove_qos(tenant_id, qos_id)

    def rename_qos(self, tenant_id, qos_id, new_name):
        """Rename QoS level"""
        LOG.debug("rename_qos() called\n")
        try:
            qos_level = cdb.get_qos(tenant_id, qos_id)
        except Exception:
            raise cexc.QosNotFound(tenant_id=tenant_id,
                                   qos_id=qos_id)
        qos = cdb.update_qos(tenant_id, qos_id, new_name)
        return qos

    def get_all_credentials(self, tenant_id):
        """Get all credentials"""
        LOG.debug("get_all_credentials() called\n")
        credential_list = cdb.get_all_credentials(tenant_id)
        return credential_list

    def get_credential_details(self, tenant_id, credential_id):
        """Get a particular credential"""
        LOG.debug("get_credential_details() called\n")
        try:
            credential = cdb.get_credential(tenant_id, credential_id)
        except Exception:
            raise cexc.CredentialNotFound(tenant_id=tenant_id,
                                          credential_id=credential_id)
        return credential

    def create_credential(self, tenant_id, credential_name, user_name,
                          password):
        """Create a new credential"""
        LOG.debug("create_credential() called\n")
        credential = cdb.add_credential(tenant_id, credential_name,
                                        user_name, password)
        return credential

    def delete_credential(self, tenant_id, credential_id):
        """Delete a credential"""
        LOG.debug("delete_credential() called\n")
        try:
            credential = cdb.get_credential(tenant_id, credential_id)
        except Exception:
            raise cexc.CredentialNotFound(tenant_id=tenant_id,
                                          credential_id=credential_id)
        credential = cdb.remove_credential(tenant_id, credential_id)
        return credential

    def rename_credential(self, tenant_id, credential_id, new_name):
        """Rename the particular credential resource"""
        LOG.debug("rename_credential() called\n")
        try:
            credential = cdb.get_credential(tenant_id, credential_id)
        except Exception:
            raise cexc.CredentialNotFound(tenant_id=tenant_id,
                                          credential_id=credential_id)
        credential = cdb.update_credential(tenant_id, credential_id, new_name)
        return credential

    def schedule_host(self, tenant_id, instance_id, instance_desc):
        """Provides the hostname on which a dynamic vnic is reserved"""
        LOG.debug("schedule_host() called\n")
        host_list = self._invoke_device_plugins(self._func_name(),
                                                [tenant_id,
                                                 instance_id,
                                                 instance_desc])
        return host_list

    def associate_port(self, tenant_id, instance_id, instance_desc):
        """
        Get the portprofile name and the device name for the dynamic vnic
        """
        LOG.debug("associate_port() called\n")
        return self._invoke_device_plugins(self._func_name(), [tenant_id,
                                                               instance_id,
                                                               instance_desc])

    def detach_port(self, tenant_id, instance_id, instance_desc):
        """
        Remove the association of the VIF with the dynamic vnic
        """
        LOG.debug("detach_port() called\n")
        return self._invoke_device_plugins(self._func_name(), [tenant_id,
                                                               instance_id,
                                                               instance_desc])

    def create_multiport(self, tenant_id, net_id_list, port_state, ports_desc):
        """
        Creates multiple ports on the specified Virtual Network.
        """
        LOG.debug("create_ports() called\n")
        ports_num = len(net_id_list)
        ports_id_list = []
        ports_dict_list = []

        for net_id in net_id_list:
            db.validate_network_ownership(tenant_id, net_id)
            port = db.port_create(net_id, port_state)
            ports_id_list.append(port[const.UUID])
            port_dict = {const.PORT_ID: port[const.UUID]}
            ports_dict_list.append(port_dict)

        self._invoke_device_plugins(self._func_name(), [tenant_id,
                                                        net_id_list,
                                                        ports_num,
                                                        ports_id_list])
        return ports_dict_list

    """
    Private functions
    """
    def _invoke_device_plugins(self, function_name, args):
        """
        All device-specific calls are delegated to the model
        """
        return getattr(self._model, function_name)(args)

    def _get_vlan_for_tenant(self, tenant_id, net_name):
        """Get vlan ID"""
        return self._vlan_mgr.reserve_segmentation_id(tenant_id, net_name)

    def _release_vlan_for_tenant(self, tenant_id, net_id):
        """Relase VLAN"""
        return self._vlan_mgr.release_segmentation_id(tenant_id, net_id)

    def _get_vlan_name(self, net_id, vlan):
        """Getting the vlan name from the tenant and vlan"""
        vlan_name = conf.VLAN_NAME_PREFIX + vlan
        return vlan_name

    def _validate_port_state(self, port_state):
        """Checking the port state"""
        if port_state.upper() not in (const.PORT_UP, const.PORT_DOWN):
            raise exc.StateInvalid(port_state=port_state)
        return True

    def _func_name(self, offset=0):
        """Getting the name of the calling funciton"""
        return inspect.stack()[1 + offset][3]
