# Copyright (c) 2016 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 os
import sys

from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
from oslo_service import service

from neutron._i18n import _LE, _LI
from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils
from neutron.agent import securitygroups_rpc as sg_rpc
from neutron.common import config as common_config
from neutron.common import constants
from neutron.common import topics
from neutron.common import utils as n_utils
from neutron.plugins.common import constants as p_constants
from neutron.plugins.ml2.drivers.agent import _agent_manager_base as amb
from neutron.plugins.ml2.drivers.agent import _common_agent as ca
from neutron.plugins.ml2.drivers.macvtap.agent import config  # noqa
from neutron.plugins.ml2.drivers.macvtap import macvtap_common

LOG = logging.getLogger(__name__)

MACVTAP_AGENT_BINARY = "neutron-macvtap-agent"
MACVTAP_FS = "/sys/class/net/"
EXTENSION_DRIVER_TYPE = 'macvtap'


class MacvtapRPCCallBack(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
                         amb.CommonAgentManagerRpcCallBackBase):
    # Set RPC API version to 1.0 by default.
    # history
    #   1.1 Support Security Group RPC
    #   1.3 Added param devices_to_update to security_groups_provider_updated
    #   1.4 Added support for network_update
    target = oslo_messaging.Target(version='1.4')

    def network_delete(self, context, **kwargs):
        LOG.debug("network_delete received")
        network_id = kwargs.get('network_id')

        if network_id not in self.network_map:
            LOG.error(_LE("Network %s is not available."), network_id)
            return

        segment = self.network_map.get(network_id)
        if segment and segment.network_type == p_constants.TYPE_VLAN:
            if_mappings = self.agent.mgr.interface_mappings
            vlan_device_name = macvtap_common.get_vlan_device_name(
                if_mappings[segment.physical_network],
                str(segment.segmentation_id))
            ip_dev = ip_lib.IPDevice(vlan_device_name)
            if ip_dev.exists():
                LOG.debug("Delete %s", ip_dev.name)
                ip_dev.link.delete()
            else:
                LOG.debug("Cannot delete vlan device %s; it does not exist",
                          vlan_device_name)

    def port_update(self, context, **kwargs):
        port = kwargs['port']
        LOG.debug("port_update received for port %s ", port)
        mac = port['mac_address']
        # Put the device name in the updated_devices set.
        # Do not store port details, as if they're used for processing
        # notifications there is no guarantee the notifications are
        # processed in the same order as the relevant API requests.
        self.updated_devices.add(mac)


class MacvtapManager(amb.CommonAgentManagerBase):
    def __init__(self, interface_mappings):
        self.interface_mappings = interface_mappings
        self.validate_interface_mappings()
        self.mac_device_name_mappings = dict()

    def validate_interface_mappings(self):
        for physnet, interface in self.interface_mappings.items():
            if not ip_lib.device_exists(interface):
                LOG.error(_LE("Interface %(intf)s for physical network "
                              "%(net)s does not exist. Agent terminated!"),
                          {'intf': interface, 'net': physnet})
                sys.exit(1)

    def ensure_port_admin_state(self, device, admin_state_up):
        LOG.debug("Setting admin_state_up to %s for device %s",
                  admin_state_up, device)
        dev = ip_lib.IPDevice(self.mac_device_name_mappings[device])
        if admin_state_up:
            dev.link.set_up()
        else:
            dev.link.set_down()

    def get_agent_configurations(self):
        return {'interface_mappings': self.interface_mappings}

    def get_agent_id(self):
        devices = ip_lib.IPWrapper().get_devices(True)
        if devices:
            mac = utils.get_interface_mac(devices[0].name)
            return 'macvtap%s' % mac.replace(":", "")
        else:
            LOG.error(_LE("Unable to obtain MAC address for unique ID. "
                          "Agent terminated!"))
            sys.exit(1)

    def get_devices_modified_timestamps(self, devices):
        # TODO(kevinbenton): this should be implemented to detect
        # rapid Nova instance rebuilds.
        return {}

    def get_all_devices(self):
        devices = set()
        all_device_names = os.listdir(MACVTAP_FS)
        # Refresh the mac_device_name mapping
        self.mac_device_name_mappings = dict()
        for device_name in all_device_names:
            if device_name.startswith(constants.MACVTAP_DEVICE_PREFIX):
                mac = utils.get_interface_mac(device_name)
                self.mac_device_name_mappings[mac] = device_name
                devices.add(mac)
        return devices

    def get_extension_driver_type(self):
        return EXTENSION_DRIVER_TYPE

    def get_rpc_callbacks(self, context, agent, sg_agent):
        return MacvtapRPCCallBack(context, agent, sg_agent)

    def get_rpc_consumers(self):
        consumers = [[topics.PORT, topics.UPDATE],
                     [topics.NETWORK, topics.DELETE],
                     [topics.SECURITY_GROUP, topics.UPDATE]]
        return consumers

    def plug_interface(self, network_id, network_segment, device,
                       device_owner):
        # Setting ALLMULTICAST Flag on macvtap device to allow the guest
        # receiving traffic for arbitrary multicast addresses.
        # The alternative would be to let libvirt instantiate the macvtap
        # device with the 'trustGuestRxFilters' option. But doing so, the guest
        # would be able to change its mac address and therefore the mac
        # address of the macvtap device.
        dev = ip_lib.IPDevice(self.mac_device_name_mappings[device])
        dev.link.set_allmulticast_on()
        return True

    def setup_arp_spoofing_protection(self, device, device_details):
        pass

    def delete_arp_spoofing_protection(self, devices):
        pass

    def delete_unreferenced_arp_protection(self, current_devices):
        pass


def parse_interface_mappings():
    try:
        interface_mappings = n_utils.parse_mappings(
            cfg.CONF.macvtap.physical_interface_mappings)
        LOG.info(_LI("Interface mappings: %s"), interface_mappings)
        return interface_mappings
    except ValueError as e:
        LOG.error(_LE("Parsing physical_interface_mappings failed: %s. "
                      "Agent terminated!"), e)
        sys.exit(1)


def validate_firewall_driver():
    fw_driver = cfg.CONF.SECURITYGROUP.firewall_driver
    if fw_driver != 'neutron.agent.firewall.NoopFirewallDriver':
        LOG.error(_LE('Unsupported configuration option for "SECURITYGROUP.'
                      'firewall_driver"! Only "neutron.agent.firewall.'
                      'NoopFirewallDriver" is supported by macvtap agent, but'
                      '"%s" is configured. Agent terminated!'),
                  fw_driver)
        sys.exit(1)


def main():
    common_config.init(sys.argv[1:])

    common_config.setup_logging()

    validate_firewall_driver()
    interface_mappings = parse_interface_mappings()

    manager = MacvtapManager(interface_mappings)

    polling_interval = cfg.CONF.AGENT.polling_interval
    quitting_rpc_timeout = cfg.CONF.AGENT.quitting_rpc_timeout
    agent = ca.CommonAgentLoop(manager, polling_interval,
                               quitting_rpc_timeout,
                               constants.AGENT_TYPE_MACVTAP,
                               MACVTAP_AGENT_BINARY)
    LOG.info(_LI("Agent initialized successfully, now running... "))
    launcher = service.launch(cfg.CONF, agent)
    launcher.wait()
