# Copyright 2014 IBM Corp.
#
# 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.


import socket
import sys
import time

import eventlet
eventlet.monkey_patch()

from oslo.config import cfg

from neutron.agent.linux import ip_lib
from neutron.agent.linux import ovs_lib
from neutron.agent import rpc as agent_rpc
from neutron.common import config as common_config
from neutron.common import constants as n_const
from neutron.common import rpc as n_rpc
from neutron.common import topics
from neutron.common import utils as n_utils
from neutron import context
from neutron.openstack.common import log as logging
from neutron.openstack.common import loopingcall
from neutron.plugins.ibm.common import config  # noqa
from neutron.plugins.ibm.common import constants


LOG = logging.getLogger(__name__)


class SdnvePluginApi(agent_rpc.PluginApi):

    def sdnve_info(self, context, info):
        return self.call(context,
                         self.make_msg('sdnve_info', info=info))


class SdnveNeutronAgent(n_rpc.RpcCallback):

    RPC_API_VERSION = '1.1'

    def __init__(self, integ_br, interface_mappings,
                 info, root_helper, polling_interval,
                 controller_ip, reset_br, out_of_band):
        '''The agent initialization.

        Sets the following parameters and sets up the integration
        bridge and physical interfaces if need be.
        :param integ_br: name of the integration bridge.
        :param interface_mappings: interfaces to physical networks.
        :param info: local IP address of this hypervisor.
        :param root_helper: utility to use when running shell cmds.
        :param polling_interval: interval (secs) to poll DB.
        :param controller_ip: Ip address of SDN-VE controller.
        '''

        super(SdnveNeutronAgent, self).__init__()
        self.root_helper = root_helper
        self.int_bridge_name = integ_br
        self.controller_ip = controller_ip
        self.interface_mappings = interface_mappings
        self.polling_interval = polling_interval
        self.info = info
        self.reset_br = reset_br
        self.out_of_band = out_of_band

        self.agent_state = {
            'binary': 'neutron-sdnve-agent',
            'host': cfg.CONF.host,
            'topic': n_const.L2_AGENT_TOPIC,
            'configurations': {'interface_mappings': interface_mappings,
                               'reset_br': self.reset_br,
                               'out_of_band': self.out_of_band,
                               'controller_ip': self.controller_ip},
            'agent_type': n_const.AGENT_TYPE_SDNVE,
            'start_flag': True}

        if self.int_bridge_name:
            self.int_br = self.setup_integration_br(integ_br, reset_br,
                                                    out_of_band,
                                                    self.controller_ip)
            self.setup_physical_interfaces(self.interface_mappings)
        else:
            self.int_br = None

        self.setup_rpc()

    def _report_state(self):
        try:
            self.state_rpc.report_state(self.context,
                                        self.agent_state)
            self.agent_state.pop('start_flag', None)
        except Exception:
            LOG.exception(_("Failed reporting state!"))

    def setup_rpc(self):
        if self.int_br:
            mac = self.int_br.get_local_port_mac()
            self.agent_id = '%s%s' % ('sdnve', (mac.replace(":", "")))
        else:
            nameaddr = socket.gethostbyname(socket.gethostname())
            self.agent_id = '%s%s' % ('sdnve_', (nameaddr.replace(".", "_")))

        self.topic = topics.AGENT
        self.plugin_rpc = SdnvePluginApi(topics.PLUGIN)
        self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN)

        self.context = context.get_admin_context_without_session()
        self.endpoints = [self]
        consumers = [[constants.INFO, topics.UPDATE]]

        self.connection = agent_rpc.create_consumers(self.endpoints,
                                                     self.topic,
                                                     consumers)
        if self.polling_interval:
            heartbeat = loopingcall.FixedIntervalLoopingCall(
                self._report_state)
            heartbeat.start(interval=self.polling_interval)

    # Plugin calls the agents through the following
    def info_update(self, context, **kwargs):
        LOG.debug(_("info_update received"))
        info = kwargs.get('info', {})
        new_controller = info.get('new_controller')
        out_of_band = info.get('out_of_band')
        if self.int_br and new_controller:
            LOG.debug(_("info_update received. New controller"
                        "is to be set to: %s"), new_controller)
            self.int_br.run_vsctl(["set-controller",
                                   self.int_bridge_name,
                                   "tcp:" + new_controller])
            if out_of_band:
                LOG.debug(_("info_update received. New controller"
                            "is set to be out of band"))
                self.int_br.set_db_attribute("controller",
                                             self.int_bridge_name,
                                             "connection-mode",
                                             "out-of-band")

    def setup_integration_br(self, bridge_name, reset_br, out_of_band,
                             controller_ip=None):
        '''Sets up the integration bridge.

        Create the bridge and remove all existing flows if reset_br is True.
        Otherwise, creates the bridge if not already existing.
        :param bridge_name: the name of the integration bridge.
        :param reset_br: A boolean to rest the bridge if True.
        :param out_of_band: A boolean indicating controller is out of band.
        :param controller_ip: IP address to use as the bridge controller.
        :returns: the integration bridge
        '''

        int_br = ovs_lib.OVSBridge(bridge_name, self.root_helper)
        if reset_br:
            int_br.reset_bridge()
            int_br.remove_all_flows()
        else:
            int_br.create()

        # set the controller
        if controller_ip:
            int_br.run_vsctl(
                ["set-controller", bridge_name, "tcp:" + controller_ip])
        if out_of_band:
            int_br.set_db_attribute("controller", bridge_name,
                                    "connection-mode", "out-of-band")

        return int_br

    def setup_physical_interfaces(self, interface_mappings):
        '''Sets up the physical network interfaces.

        Link physical interfaces to the integration bridge.
        :param interface_mappings: map physical net names to interface names.
        '''

        for physical_network, interface in interface_mappings.iteritems():
            LOG.info(_("Mapping physical network %(physical_network)s to "
                       "interface %(interface)s"),
                     {'physical_network': physical_network,
                      'interface': interface})
            # Connect the physical interface to the bridge
            if not ip_lib.device_exists(interface, self.root_helper):
                LOG.error(_("Interface %(interface)s for physical network "
                            "%(physical_network)s does not exist. Agent "
                            "terminated!"),
                          {'physical_network': physical_network,
                           'interface': interface})
                raise SystemExit(1)
            self.int_br.add_port(interface)

    def sdnve_info(self):
        details = self.plugin_rpc.sdnve_info(
            self.context,
            {'info': self.info})
        return details

    def rpc_loop(self):

        while True:
            start = time.time()
            LOG.debug(_("Agent in the rpc loop."))

            # sleep till end of polling interval
            elapsed = (time.time() - start)
            if (elapsed < self.polling_interval):
                time.sleep(self.polling_interval - elapsed)
            else:
                LOG.info(_("Loop iteration exceeded interval "
                           "(%(polling_interval)s vs. %(elapsed)s)!"),
                         {'polling_interval': self.polling_interval,
                          'elapsed': elapsed})

    def daemon_loop(self):
        self.rpc_loop()


def create_agent_config_map(config):

    interface_mappings = n_utils.parse_mappings(
        config.SDNVE.interface_mappings)

    controller_ips = config.SDNVE.controller_ips
    LOG.info(_("Controller IPs: %s"), controller_ips)
    controller_ip = controller_ips[0]

    return {
        'integ_br': config.SDNVE.integration_bridge,
        'interface_mappings': interface_mappings,
        'controller_ip': controller_ip,
        'info': config.SDNVE.info,
        'root_helper': config.SDNVE_AGENT.root_helper,
        'polling_interval': config.SDNVE_AGENT.polling_interval,
        'reset_br': config.SDNVE.reset_bridge,
        'out_of_band': config.SDNVE.out_of_band}


def main():
    cfg.CONF.register_opts(ip_lib.OPTS)
    common_config.init(sys.argv[1:])
    common_config.setup_logging()

    try:
        agent_config = create_agent_config_map(cfg.CONF)
    except ValueError as e:
        LOG.exception(_("%s Agent terminated!"), e)
        raise SystemExit(1)

    plugin = SdnveNeutronAgent(**agent_config)

    # Start everything.
    LOG.info(_("Agent initialized successfully, now running... "))
    plugin.daemon_loop()
