# Copyright (c) 2015 SONATA-NFV and Paderborn University
# 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.
#
# Neither the name of the SONATA-NFV, Paderborn University
# nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# This work has been performed in the framework of the SONATA project,
# funded by the European Commission under Grant number 671517 through
# the Horizon 2020 and 5G-PPP programmes. The authors would like to
# acknowledge the contributions of their colleagues of the SONATA
# partner consortium (www.sonata-nfv.eu).
import json
import logging
import copy

from mininet.node import OVSSwitch

from flask import Flask
from flask import Response, request
from flask_restful import Api, Resource


class ChainApi(Resource):
    """
    The chain API is a component that is not used in OpenStack.
    It is a custom built REST API that can be used to create network chains and loadbalancers.
    """

    def __init__(self, inc_ip, inc_port, manage):
        # setup Flask
        self.app = Flask(__name__)
        self.api = Api(self.app)
        self.ip = inc_ip
        self.port = inc_port
        self.manage = manage
        self.playbook_file = '/tmp/son-emu-requests.log'
        self.api.add_resource(ChainVersionsList, "/",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(ChainList, "/v1/chain/list",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(ChainVnfInterfaces, "/v1/chain/<src_vnf>/<src_intfs>/<dst_vnf>/<dst_intfs>",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(ChainVnfDcStackInterfaces,
                              "/v1/chain/<src_dc>/<src_stack>/<src_vnf>/<src_intfs>/<dst_dc>/<dst_stack>/<dst_vnf>/<dst_intfs>",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(BalanceHostList, "/v1/lb/list",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(BalanceHost, "/v1/lb/<vnf_src_name>/<vnf_src_interface>",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(BalanceHostDcStack, "/v1/lb/<src_dc>/<src_stack>/<vnf_src_name>/<vnf_src_interface>",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(QueryTopology, "/v1/topo",
                              resource_class_kwargs={'api': self})
        self.api.add_resource(Shutdown, "/shutdown")

        @self.app.after_request
        def add_access_control_header(response):
            response.headers['Access-Control-Allow-Origin'] = '*'
            return response

    def _start_flask(self):
        logging.info("Starting %s endpoint @ http://%s:%d" %
                     ("ChainDummyApi", self.ip, self.port))
        if self.app is not None:
            self.app.before_request(self.dump_playbook)
            self.app.run(self.ip, self.port, debug=True, use_reloader=False)

    def dump_playbook(self):
        with self.manage.lock:
            with open(self.playbook_file, 'a') as logfile:
                if len(request.data) > 0:
                    data = "# CHAIN API\n"
                    data += "curl -X {type} -H \"Content-type: application/json\" -d '{data}' {url}".format(type=request.method,
                                                                                                            data=request.data,
                                                                                                            url=request.url)
                    logfile.write(data + "\n")


class Shutdown(Resource):
    def get(self):
        logging.debug(("%s is beeing shut down") % (__name__))
        func = request.environ.get('werkzeug.server.shutdown')
        if func is None:
            raise RuntimeError('Not running with the Werkzeug Server')
        func()


class ChainVersionsList(Resource):
    '''
    Entrypoint to find versions of the chain api.
    '''

    def __init__(self, api):
        self.api = api

    def get(self):
        '''
        :return: flask.Response containing the openstack like description of the chain api
        '''
        # at least let it look like an open stack function
        try:
            resp = """
                {
                    "versions": [
                        {
                            "id": "v1",
                            "links": [
                                {
                                    "href": "http://%s:%d/v1/",
                                    "rel": "self"
                                }
                            ],
                            "status": "CURRENT",
                            "version": "1",
                            "min_version": "1",
                            "updated": "2013-07-23T11:33:21Z"
                        }
                    ]
                }
            """ % (self.api.ip, self.api.port)

            return Response(resp, status=200, mimetype="application/json")

        except Exception as ex:
            logging.exception(
                u"%s: Could not show list of versions." % __name__)
            return ex.message, 500


class ChainList(Resource):
    '''
    Will retrieve all chains including their paths.
    '''

    def __init__(self, api):
        self.api = api

    def get(self):
        '''
        :return: flask.Response containing all live chains
        '''
        # at least let it look like an open stack function
        try:
            resp = {"chains": list()}

            for chain in self.api.manage.full_chain_data.values():
                resp["chains"].append(chain)

            return Response(json.dumps(resp), status=200,
                            mimetype="application/json")

        except Exception as ex:
            logging.exception(
                u"%s: Could not list all network chains." % __name__)
            return ex.message, 500


class BalanceHostList(Resource):
    '''
    Will retrieve all loadbalance rules including their paths.
    '''

    def __init__(self, api):
        self.api = api

    def get(self):
        '''
        :return: flask.Response containing all live loadbalancer rules
        '''
        # at least let it look like an open stack function
        try:
            resp = {"loadbalancers": list()}

            for lb in self.api.manage.full_lb_data.values():
                resp["loadbalancers"].append(lb)

            return Response(json.dumps(resp), status=200,
                            mimetype="application/json")

        except Exception as ex:
            logging.exception(
                u"%s: Could not list all live loadbalancers." % __name__)
            return ex.message, 500


class ChainVnfInterfaces(Resource):
    """
    Handles requests targeted at: "/v1/chain/<src_vnf>/<src_intfs>/<dst_vnf>/<dst_intfs>"
    Requests are for tearing down or setting up a chain between two vnfs
    """

    def __init__(self, api):
        self.api = api

    def put(self, src_vnf, src_intfs, dst_vnf, dst_intfs):
        """
        A put request to "/v1/chain/<src_vnf>/<src_intfs>/<dst_vnf>/<dst_intfs>"
        will create a chain between two interfaces at the specified vnfs.

        Note:
           Does not allow a custom path. Uses ``.post``
           Internally just makes a POST request with no POST data!

        :param src_vnf: Name of the source VNF
        :type src_vnf: ``str``
        :param src_intfs: Name of the source VNF interface to chain on
        :type src_intfs: ``str``
        :param dst_vnf: Name of the destination VNF
        :type dst_vnf: ``str``
        :param dst_intfs: Name of the destination VNF interface to chain on
        :type dst_intfs: ``str``
        :return: flask.Response 200 if set up correctly else 500 also returns the cookie as dict {'cookie': value}
         501 if one of the VNF / intfs does not exist
        :rtype: :class:`flask.Response`
        """
        return self.post(src_vnf, src_intfs, dst_vnf, dst_intfs)

    def post(self, src_vnf, src_intfs, dst_vnf, dst_intfs):
        """
         A post request to "/v1/chain/<src_vnf>/<src_intfs>/<dst_vnf>/<dst_intfs>"
         will create a chain between two interfaces at the specified vnfs.
         The POST data contains the path like this.
         { "path": ["dc1.s1", "s1", "dc4.s1"]}
         path specifies the destination vnf and interface and contains a list of switches
         that the path traverses. The path may not contain single hop loops like:
         [s1, s2, s1].
         This is a limitation of Ryu, as Ryu does not allow the `INPUT_PORT` action!

        :param src_vnf: Name of the source VNF
        :type src_vnf: ``str``
        :param src_intfs: Name of the source VNF interface to chain on
        :type src_intfs: ``str``
        :param dst_vnf: Name of the destination VNF
        :type dst_vnf: ``str``
        :param dst_intfs: Name of the destination VNF interface to chain on
        :type dst_intfs: ``str``
        :return: flask.Response 200 if set up correctly else 500 also returns the cookie as dict {'cookie': value}
         501 if one of the VNF / intfs does not exist
        :rtype: :class:`flask.Response`

        """

        if request.is_json:
            path = request.json.get('path')
            layer2 = request.json.get('layer2', True)
        else:
            path = None
            layer2 = True

        # check if both VNFs exist
        if not self.api.manage.check_vnf_intf_pair(src_vnf, src_intfs):
            return Response(u"VNF %s or intfs %s does not exist" % (src_vnf, src_intfs), status=501,
                            mimetype="application/json")
        if not self.api.manage.check_vnf_intf_pair(dst_vnf, dst_intfs):
            return Response(u"VNF %s or intfs %s does not exist" % (dst_vnf, dst_intfs), status=501,
                            mimetype="application/json")
        try:
            cookie = self.api.manage.network_action_start(src_vnf, dst_vnf, vnf_src_interface=src_intfs,
                                                          vnf_dst_interface=dst_intfs, bidirectional=True,
                                                          path=path, layer2=layer2)
            resp = {'cookie': cookie}
            return Response(json.dumps(resp), status=200,
                            mimetype="application/json")

        except Exception as e:
            logging.exception(
                u"%s: Error setting up the chain.\n %s" % (__name__, e))
            return Response(u"Error setting up the chain",
                            status=500, mimetype="application/json")

    def delete(self, src_vnf, src_intfs, dst_vnf, dst_intfs):
        """
        A DELETE request to "/v1/chain/<src_vnf>/<src_intfs>/<dst_vnf>/<dst_intfs>"
        will delete a previously created chain.

        :param src_vnf: Name of the source VNF
        :type src_vnf: ``str``
        :param src_intfs: Name of the source VNF interface to chain on
        :type src_intfs: ``str``
        :param dst_vnf: Name of the destination VNF
        :type dst_vnf: ``str``
        :param dst_intfs: Name of the destination VNF interface to chain on
        :type dst_intfs: ``str``
        :return: flask.Response 200 if set up correctly else 500\
         also returns the cookie as dict {'cookie': value}
         501 if one of the VNF / intfs does not exist
        :rtype: :class:`flask.Response`

        """
        # check if both VNFs exist
        # check if both VNFs exist
        if not self.api.manage.check_vnf_intf_pair(src_vnf, src_intfs):
            return Response(u"VNF %s or intfs %s does not exist" % (src_vnf, src_intfs), status=501,
                            mimetype="application/json")
        if not self.api.manage.check_vnf_intf_pair(dst_vnf, dst_intfs):
            return Response(u"VNF %s or intfs %s does not exist" % (dst_vnf, dst_intfs), status=501,
                            mimetype="application/json")
        try:
            cookie = self.api.manage.network_action_stop(src_vnf, dst_vnf, vnf_src_interface=src_intfs,
                                                         vnf_dst_interface=dst_intfs, bidirectional=True)
            return Response(json.dumps(cookie), status=200,
                            mimetype="application/json")
        except Exception as e:
            logging.exception(
                u"%s: Error deleting the chain.\n %s" % (__name__, e))
            return Response(u"Error deleting the chain",
                            status=500, mimetype="application/json")


class ChainVnfDcStackInterfaces(Resource):
    '''
    Handles requests targeted at: "/v1/chain/<src_dc>/<src_stack>/<src_vnf>/<src_intfs>/<dst_dc>/<dst_stack>/<dst_vnf>/<dst_intfs>"
    Handles tearing down or setting up a chain between two vnfs for stacks.
    '''

    def __init__(self, api):
        self.api = api

    def put(self, src_dc, src_stack, src_vnf, src_intfs,
            dst_dc, dst_stack, dst_vnf, dst_intfs):
        """
        A PUT request to "/v1/chain/<src_dc>/<src_stack>/<src_vnf>/<src_intfs>/<dst_dc>/<dst_stack>/<dst_vnf>/<dst_intfs>"
        will set up chain.

        :Note: PUT Requests can not set up custom paths!

        :param src_dc: Name of the source datacenter
        :type src_dc: `str`
        :param src_stack: Name of the source stack
        :type src_stack: `str`
        :param src_vnf: Name of the source VNF
        :type src_vnf: ``str``
        :param src_intfs: Name of the source VNF interface to chain on
        :type src_intfs: ``str``
        :param dst_dc: Name of the destination datacenter
        :type dst_dc: ``str``
        :param dst_stack: Name of the destination stack
        :type dst_stack: ``str``
        :param dst_vnf: Name of the destination VNF
        :type dst_vnf: ``str``
        :param dst_intfs: Name of the destination VNF interface to chain on
        :type dst_intfs: ``str``
        :return: flask.Response 200 if set up correctly else 500\
         also returns the cookie as dict {'cookie': value}
         501 if VNF or intfs does not exist
        :rtype: :class:`flask.Response`

        """
        # search for real names
        real_names = self._findNames(
            src_dc, src_stack, src_vnf, src_intfs, dst_dc, dst_stack, dst_vnf, dst_intfs)
        if not isinstance(real_names, tuple):
            # something went wrong
            return real_names

        container_src, container_dst, interface_src, interface_dst = real_names

        # check if both VNFs exist
        if not self.api.manage.check_vnf_intf_pair(
                container_src, interface_src):
            return Response(u"VNF %s or intfs %s does not exist" % (container_src, interface_src), status=501,
                            mimetype="application/json")
        if not self.api.manage.check_vnf_intf_pair(
                container_dst, interface_dst):
            return Response(u"VNF %s or intfs %s does not exist" % (container_dst, interface_dst), status=501,
                            mimetype="application/json")

        try:
            cookie = self.api.manage.network_action_start(container_src, container_dst, vnf_src_interface=interface_src,
                                                          vnf_dst_interface=interface_dst, bidirectional=True,
                                                          layer2=True)
            resp = {'cookie': cookie}
            return Response(json.dumps(resp), status=200,
                            mimetype="application/json")

        except Exception as e:
            logging.exception(
                u"%s: Error setting up the chain.\n %s" % (__name__, e))
            return Response(u"Error setting up the chain",
                            status=500, mimetype="application/json")

    def post(self, src_dc, src_stack, src_vnf, src_intfs,
             dst_dc, dst_stack, dst_vnf, dst_intfs):
        """
         A post request to "/v1/chain/<src_dc>/<src_stack>/<src_vnf>/<src_intfs>/<dst_dc>/<dst_stack>/<dst_vnf>/<dst_intfs>"
         will create a chain between two interfaces at the specified vnfs.
         The POST data contains the path like this.
         { "path": ["dc1.s1", "s1", "dc4.s1"]}
         path specifies the destination vnf and interface and contains a list of switches
         that the path traverses. The path may not contain single hop loops like:
         [s1, s2, s1].
         This is a limitation of Ryu, as Ryu does not allow the `INPUT_PORT` action!

        :param src_vnf: Name of the source VNF
        :type src_vnf: ``str``
        :param src_intfs: Name of the source VNF interface to chain on
        :type src_intfs: ``str``
        :param dst_vnf: Name of the destination VNF
        :type dst_vnf: ``str``
        :param dst_intfs: Name of the destination VNF interface to chain on
        :type dst_intfs: ``str``
        :return: flask.Response 200 if set up correctly else 500 also returns the cookie as dict {'cookie': value}
         501 if vnf / intfs do not exist
        :rtype: :class:`flask.Response`

        """
        if request.is_json:
            path = request.json.get('path')
            layer2 = request.json.get('layer2', True)
        else:
            path = None
            layer2 = True

        # search for real names
        real_names = self._findNames(
            src_dc, src_stack, src_vnf, src_intfs, dst_dc, dst_stack, dst_vnf, dst_intfs)
        if not isinstance(real_names, tuple):
            # something went wrong
            return real_names

        container_src, container_dst, interface_src, interface_dst = real_names

        try:
            cookie = self.api.manage.network_action_start(container_src, container_dst, vnf_src_interface=interface_src,
                                                          vnf_dst_interface=interface_dst, bidirectional=True,
                                                          path=path, layer2=layer2)
            resp = {'cookie': cookie}
            return Response(json.dumps(resp), status=200,
                            mimetype="application/json")

        except Exception as e:
            logging.exception(
                u"%s: Error setting up the chain.\n %s" % (__name__, e))
            return Response(u"Error setting up the chain",
                            status=500, mimetype="application/json")

    def delete(self, src_dc, src_stack, src_vnf, src_intfs,
               dst_dc, dst_stack, dst_vnf, dst_intfs):
        """
        A DELETE request to "/v1/chain/<src_dc>/<src_stack>/<src_vnf>/<src_intfs>/<dst_dc>/<dst_stack>/<dst_vnf>/<dst_intfs>"
        will delete a previously created chain.

        :param src_dc: Name of the source datacenter
        :type src_dc: `str`
        :param src_stack: Name of the source stack
        :type src_stack: `str`
        :param src_vnf: Name of the source VNF
        :type src_vnf: ``str``
        :param src_intfs: Name of the source VNF interface to chain on
        :type src_intfs: ``str``
        :param dst_dc: Name of the destination datacenter
        :type dst_dc: ``str``
        :param dst_stack: Name of the destination stack
        :type dst_stack: ``str``
        :param dst_vnf: Name of the destination VNF
        :type dst_vnf: ``str``
        :param dst_intfs: Name of the destination VNF interface to chain on
        :type dst_intfs: ``str``
        :return: flask.Response 200 if set up correctly else 500\
         also returns the cookie as dict {'cookie': value}
         501 if one of the VNF / intfs does not exist
        :rtype: :class:`flask.Response`

        """
        # search for real names
        real_names = self._findNames(
            src_dc, src_stack, src_vnf, src_intfs, dst_dc, dst_stack, dst_vnf, dst_intfs)
        if not isinstance(real_names, tuple):
            # something went wrong, real_names is a Response object
            return real_names

        container_src, container_dst, interface_src, interface_dst = real_names

        try:
            cookie = self.api.manage.network_action_stop(container_src, container_dst, vnf_src_interface=interface_src,
                                                         vnf_dst_interface=interface_dst, bidirectional=True)
            return Response(json.dumps(cookie), status=200,
                            mimetype="application/json")
        except Exception as e:
            logging.exception(
                u"%s: Error deleting the chain.\n %s" % (__name__, e))
            return Response(u"Error deleting the chain",
                            status=500, mimetype="application/json")

    # Tries to find real container and interface names according to heat template names
    # Returns a tuple of 4 or a Response object
    def _findNames(self, src_dc, src_stack, src_vnf, src_intfs,
                   dst_dc, dst_stack, dst_vnf, dst_intfs):
        # search for datacenters
        if src_dc not in self.api.manage.net.dcs or dst_dc not in self.api.manage.net.dcs:
            return Response(u"At least one DC does not exist",
                            status=500, mimetype="application/json")
        dc_src = self.api.manage.net.dcs[src_dc]
        dc_dst = self.api.manage.net.dcs[dst_dc]
        # search for related OpenStackAPIs
        api_src = None
        api_dst = None
        from openstack_api_endpoint import OpenstackApiEndpoint
        for api in OpenstackApiEndpoint.dc_apis:
            if api.compute.dc == dc_src:
                api_src = api
            if api.compute.dc == dc_dst:
                api_dst = api
        if api_src is None or api_dst is None:
            return Response(u"At least one OpenStackAPI does not exist",
                            status=500, mimetype="application/json")
        # search for stacks
        stack_src = None
        stack_dst = None
        for stack in api_src.compute.stacks.values():
            if stack.stack_name == src_stack:
                stack_src = stack
        for stack in api_dst.compute.stacks.values():
            if stack.stack_name == dst_stack:
                stack_dst = stack
        if stack_src is None or stack_dst is None:
            return Response(u"At least one Stack does not exist",
                            status=500, mimetype="application/json")
        # search for servers
        server_src = None
        server_dst = None
        for server in stack_src.servers.values():
            if server.template_name == src_vnf:
                server_src = server
                break
        for server in stack_dst.servers.values():
            if server.template_name == dst_vnf:
                server_dst = server
                break
        if server_src is None or server_dst is None:
            return Response(u"At least one VNF does not exist",
                            status=500, mimetype="application/json")

        container_src = server_src.name
        container_dst = server_dst.name

        # search for ports
        port_src = None
        port_dst = None
        if src_intfs in server_src.port_names:
            port_src = stack_src.ports[src_intfs]
        if dst_intfs in server_dst.port_names:
            port_dst = stack_dst.ports[dst_intfs]
        if port_src is None or port_dst is None:
            return Response(u"At least one Port does not exist",
                            status=500, mimetype="application/json")

        interface_src = port_src.intf_name
        interface_dst = port_dst.intf_name

        return container_src, container_dst, interface_src, interface_dst


class BalanceHostDcStack(Resource):
    """
    Handles requests to "/v1/lb/<src_dc>/<src_stack>/<vnf_src_name>/<vnf_src_interface>"
    Sets up LoadBalancers for VNFs that are belonging to a certain stack.
    """

    def __init__(self, api):
        self.api = api

    def post(self, src_dc, src_stack, vnf_src_name, vnf_src_interface):
        """
        A POST request to "/v1/lb/<src_dc>/<src_stack>/<vnf_src_name>/<vnf_src_interface>"
        will set up a loadbalancer. The target VNFs and interfaces are in the post data.

        :Example:
            See :class:`heat.chain_api.BalanceHost.post`

        :param src_dc: Name of the source VNF
        :type src_dc: ``str``
        :param src_stack: Name of the source VNF interface to chain on
        :type src_stack: ``str``
         * src_stack == "floating" sets up a new floating node, so only use this name if you know what you are doing.
        :param vnf_src_name:
        :type vnf_src_name: ``str``
        :param vnf_src_interface:
        :type vnf_src_interface: ``str``
        :return: flask.Response 200 if set up correctly else 500
        :rtype: :class:`flask.Response`

        """
        try:
            req = request.json
            if req is None or len(req) == 0 or "dst_vnf_interfaces" not in req:
                return Response(u"You have to specify destination vnfs via the POST data.",
                                status=500, mimetype="application/json")

            dst_vnfs = req.get('dst_vnf_interfaces')
            container_src = None
            interface_src = None

            # check src vnf/port
            if src_stack != "floating":
                real_src = self._findName(
                    src_dc, src_stack, vnf_src_name, vnf_src_interface)
                if not isinstance(real_src, tuple):
                    # something went wrong, real_src is a Response object
                    return real_src

                container_src, interface_src = real_src

            real_dst_dict = {}
            for dst_vnf in dst_vnfs:
                dst_dc = dst_vnf.get('pop', None)
                dst_stack = dst_vnf.get('stack', None)
                dst_server = dst_vnf.get('server', None)
                dst_port = dst_vnf.get('port', None)
                if dst_dc is not None and dst_stack is not None and dst_server is not None and dst_port is not None:
                    real_dst = self._findName(
                        dst_dc, dst_stack, dst_server, dst_port)
                    if not isinstance(real_dst, tuple):
                        # something went wrong, real_dst is a Response object
                        return real_dst
                    real_dst_dict[real_dst[0]] = real_dst[1]

            input_object = {"dst_vnf_interfaces": real_dst_dict,
                            "path": req.get("path", None)}

            if src_stack != "floating":
                self.api.manage.add_loadbalancer(
                    container_src, interface_src, lb_data=input_object)
                return Response(u"Loadbalancer set up at %s:%s" % (container_src, interface_src),
                                status=200, mimetype="application/json")
            else:
                cookie, floating_ip = self.api.manage.add_floating_lb(
                    src_dc, lb_data=input_object)

                return Response(json.dumps({"cookie": "%d" % cookie, "floating_ip": "%s" % floating_ip}),
                                status=200, mimetype="application/json")

        except Exception as e:
            logging.exception(u"%s: Error setting up the loadbalancer at %s %s %s:%s.\n %s" %
                              (__name__, src_dc, src_stack, vnf_src_name, vnf_src_interface, e))
            return Response(u"%s: Error setting up the loadbalancer at %s %s %s:%s.\n %s" %
                            (__name__, src_dc, src_stack, vnf_src_name,
                             vnf_src_interface, e), status=500,
                            mimetype="application/json")

    def delete(self, src_dc, src_stack, vnf_src_name, vnf_src_interface):
        """
        Will delete a load balancer that sits behind a specified interface at a vnf for a specific stack

        :param src_dc: Name of the source VNF
        :type src_dc: ``str``
        :param src_stack: Name of the source VNF interface to chain on
        :type src_stack: ``str``
        :param vnf_src_name:
        :type vnf_src_name: ``str``
        :param vnf_src_interface:
        :type vnf_src_interface: ``str``
        :return: flask.Response 200 if set up correctly else 500
        :rtype: :class:`flask.Response`

        """
        try:
            # check src vnf/port
            if src_stack != "floating":
                real_src = self._findName(
                    src_dc, src_stack, vnf_src_name, vnf_src_interface)
                if not isinstance(real_src, tuple):
                    # something went wrong, real_src is a Response object
                    return real_src

                container_src, interface_src = real_src

                self.api.manage.delete_loadbalancer(
                    container_src, interface_src)
                return Response(u"Loadbalancer deleted at %s:%s" % (vnf_src_name, vnf_src_interface),
                                status=200, mimetype="application/json")
            else:
                cookie = vnf_src_name
                self.api.manage.delete_floating_lb(cookie)
                return Response(u"Floating loadbalancer with cookie %s deleted" % (cookie),
                                status=200, mimetype="application/json")

        except Exception as e:
            logging.exception(u"%s: Error deleting the loadbalancer at %s %s %s%s.\n %s" %
                              (__name__, src_dc, src_stack, vnf_src_name, vnf_src_interface, e))
            return Response(u"%s: Error deleting the loadbalancer at %s %s %s%s." %
                            (__name__, src_dc, src_stack, vnf_src_name,
                             vnf_src_interface), status=500,
                            mimetype="application/json")

    # Tries to find real container and port name according to heat template names
    # Returns a string or a Response object
    def _findName(self, dc, stack, vnf, port):
        # search for datacenters
        if dc not in self.api.manage.net.dcs:
            return Response(u"DC does not exist", status=500,
                            mimetype="application/json")
        dc_real = self.api.manage.net.dcs[dc]
        # search for related OpenStackAPIs
        api_real = None
        from openstack_api_endpoint import OpenstackApiEndpoint
        for api in OpenstackApiEndpoint.dc_apis:
            if api.compute.dc == dc_real:
                api_real = api
        if api_real is None:
            return Response(u"OpenStackAPI does not exist",
                            status=500, mimetype="application/json")
        # search for stacks
        stack_real = None
        for stackObj in api_real.compute.stacks.values():
            if stackObj.stack_name == stack:
                stack_real = stackObj
        if stack_real is None:
            return Response(u"Stack does not exist", status=500,
                            mimetype="application/json")
        # search for servers
        server_real = None
        for server in stack_real.servers.values():
            if server.template_name == vnf:
                server_real = server
                break
        if server_real is None:
            return Response(u"VNF does not exist", status=500,
                            mimetype="application/json")

        container_real = server_real.name

        # search for ports
        port_real = None
        if port in server_real.port_names:
            port_real = stack_real.ports[port]
        if port_real is None:
            return Response(u"At least one Port does not exist",
                            status=500, mimetype="application/json")

        interface_real = port_real.intf_name

        return container_real, interface_real


class BalanceHost(Resource):
    """
    Handles requests at "/v1/lb/<vnf_src_name>/<vnf_src_interface>"
    to set up or delete Load Balancers.
    """

    def __init__(self, api):
        self.api = api

    def post(self, vnf_src_name, vnf_src_interface):
        """
        Will set up a Load balancer behind an interface at a specified vnf
        We need both to avoid naming conflicts as interface names are not unique

        :param vnf_src_name: Name of the source VNF
        :type vnf_src_name: ``str``
        :param vnf_src_interface: Name of the source VNF interface to chain on
        :type vnf_src_interface: ``str``
        :return: flask.Response 200 if set up correctly else 500
         501 if VNF or intfs does not exist
        :rtype: :class:`flask.Response`

        """
        try:
            req = request.json
            if req is None or len(req) == 0 or "dst_vnf_interfaces" not in req:
                return Response(u"You have to specify destination vnfs via the POST data.",
                                status=500, mimetype="application/json")

            if vnf_src_name != "floating":
                # check if VNF exist
                if not self.api.manage.check_vnf_intf_pair(
                        vnf_src_name, vnf_src_interface):
                    return Response(u"VNF %s or intfs %s does not exist" % (vnf_src_name, vnf_src_interface),
                                    status=501,
                                    mimetype="application/json")
                self.api.manage.add_loadbalancer(
                    vnf_src_name, vnf_src_interface, lb_data=req)

                return Response(u"Loadbalancer set up at %s:%s" % (vnf_src_name, vnf_src_interface),
                                status=200, mimetype="application/json")
            else:
                cookie, floating_ip = self.api.manage.add_floating_lb(
                    vnf_src_interface, lb_data=req)

                return Response(json.dumps({"cookie": "%d" % cookie, "floating_ip": "%s" % floating_ip}),
                                status=200, mimetype="application/json")
        except Exception as e:
            logging.exception(u"%s: Error setting up the loadbalancer at %s:%s.\n %s" %
                              (__name__, vnf_src_name, vnf_src_interface, e))
            return Response(u"%s: Error setting up the loadbalancer at %s:%s.\n %s" %
                            (__name__, vnf_src_name, vnf_src_interface, e), status=500, mimetype="application/json")

    def delete(self, vnf_src_name, vnf_src_interface):
        """
        Will delete a load balancer that sits behind a specified interface at a vnf

        :param vnf_src_name: Name of the source VNF
        :type vnf_src_name: ``str``
        :param vnf_src_interface: Name of the source VNF interface to chain on
        :type vnf_src_interface: ``str``
        :return: flask.Response 200 if set up correctly else 500
         501 if VNF or intfs does not exist
        :rtype: :class:`flask.Response`

        """
        # check if VNF exist
        if not self.api.manage.check_vnf_intf_pair(
                vnf_src_name, vnf_src_interface):
            return Response(u"VNF %s or intfs %s does not exist" % (vnf_src_name, vnf_src_interface), status=501,
                            mimetype="application/json")
        try:
            logging.debug("Deleting loadbalancer at %s: interface: %s" %
                          (vnf_src_name, vnf_src_interface))
            net = self.api.manage.net

            if vnf_src_name != "floating":
                # check if VNF exists
                if vnf_src_name not in net:
                    return Response(u"Source VNF or interface can not be found." % vnf_src_name,
                                    status=404, mimetype="application/json")

                self.api.manage.delete_loadbalancer(
                    vnf_src_name, vnf_src_interface)

                return Response(u"Loadbalancer deleted at %s:%s" % (vnf_src_name, vnf_src_interface),
                                status=200, mimetype="application/json")
            else:
                cookie = vnf_src_name
                self.api.manage.delete_floating_lb(cookie)
                return Response(u"Floating loadbalancer with cookie %s removed" % (cookie),
                                status=200, mimetype="application/json")
        except Exception as e:
            logging.exception(u"%s: Error deleting the loadbalancer at %s%s.\n %s" %
                              (__name__, vnf_src_name, vnf_src_interface, e))
            return Response(u"%s: Error deleting the loadbalancer at %s%s." %
                            (__name__, vnf_src_name, vnf_src_interface), status=500, mimetype="application/json")


class QueryTopology(Resource):
    """
    Handles requests at "/v1/topo/"
    """

    def __init__(self, api):
        self.api = api

    def get(self):
        """
        Answers GET requests for the current network topology at "/v1/topo".
        This will only return switches and datacenters and ignore currently deployed VNFs.

        :return: 200 if successful with the network graph as json dict, else 500

        """
        try:
            logging.debug("Querying topology")
            graph = self.api.manage.net.DCNetwork_graph
            net = self.api.manage.net
            # root node is nodes
            topology = {"nodes": list()}

            for n in graph:
                # remove root node as well as the floating switch fs1
                if n != "root" and n != "fs1":
                    # we only want to return switches!
                    if not isinstance(net[n], OVSSwitch):
                        continue
                    node = dict()

                    # get real datacenter label
                    for dc in self.api.manage.net.dcs.values():
                        if str(dc.switch) == str(n):
                            node["name"] = str(n)
                            node["type"] = "Datacenter"
                            node["label"] = str(dc.label)
                            break

                    # node is not a datacenter. It has to be a switch
                    if node.get("type", "") != "Datacenter":
                        node["name"] = str(n)
                        node["type"] = "Switch"

                    node["links"] = list()
                    # add links to the topology
                    for graph_node, data in graph[n].items():
                        # only add links to the topology that connect switches
                        if isinstance(net[graph_node], OVSSwitch):
                            # we allow multiple edges between switches, so add them all
                            # with their unique keys
                            link = copy.copy(data)
                            for edge in link:
                                # do not add any links to the floating switch
                                # to the topology!
                                if graph_node == "fs1":
                                    continue
                                # the translator wants everything as a string!
                                for key, value in link[edge].items():
                                    link[edge][key] = str(value)
                                # name of the destination
                                link[edge]["name"] = graph_node
                                node["links"].append(link)

                    topology["nodes"].append(node)

            return Response(json.dumps(topology),
                            status=200, mimetype="application/json")
        except Exception as e:
            logging.exception(u"%s: Error querying topology.\n %s" %
                              (__name__, e))
            return Response(u"%s: Error querying topology.\n %s" %
                            (__name__, e), status=500, mimetype="application/json")
