# Copyright (c) 2014 OpenStack Foundation.
# 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 distutils
import re
import shutil
import tempfile

import netaddr
from neutron_lib import constants as n_consts
from neutron_lib import exceptions
from neutron_lib.utils import helpers
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import uuidutils
from oslo_utils import versionutils

from neutron.agent.common import ovs_lib
from neutron.agent.l3 import ha_router
from neutron.agent.l3 import namespaces
from neutron.agent.linux import external_process
from neutron.agent.linux import ip_lib
from neutron.agent.linux import keepalived
from neutron.agent.linux import utils as agent_utils
from neutron.common import utils as common_utils
from neutron.conf.agent.l3 import config as l3_config
from neutron.plugins.ml2.drivers.openvswitch.agent.common \
    import constants as ovs_const
from neutron.privileged.agent.linux import dhcp as priv_dhcp

LOG = logging.getLogger(__name__)


MINIMUM_DNSMASQ_VERSION = '2.67'
DNSMASQ_VERSION_DHCP_RELEASE6 = '2.76'
DNSMASQ_VERSION_HOST_ADDR6_LIST = '2.81'
DIRECT_PORT_QOS_MIN_OVS_VERSION = '2.11'
MINIMUM_DIBBLER_VERSION = '1.0.1'
CONNTRACK_GRE_MODULE = 'nf_conntrack_proto_gre'


def ovs_vxlan_supported(from_ip='192.0.2.1', to_ip='192.0.2.2'):
    br_name = common_utils.get_rand_device_name(prefix='vxlantest-')
    port_name = common_utils.get_rand_device_name(prefix='vxlantest-')
    with ovs_lib.OVSBridge(br_name) as br:
        port = br.add_tunnel_port(
            port_name=port_name,
            remote_ip=from_ip,
            local_ip=to_ip,
            tunnel_type=n_consts.TYPE_VXLAN)
        return port != ovs_lib.INVALID_OFPORT


def ovs_geneve_supported(from_ip='192.0.2.3', to_ip='192.0.2.4'):
    br_name = common_utils.get_rand_device_name(prefix='genevetest-')
    port_name = common_utils.get_rand_device_name(prefix='genevetest-')
    with ovs_lib.OVSBridge(br_name) as br:
        port = br.add_tunnel_port(
            port_name=port_name,
            remote_ip=from_ip,
            local_ip=to_ip,
            tunnel_type=n_consts.TYPE_GENEVE)
        return port != ovs_lib.INVALID_OFPORT


def iproute2_vxlan_supported():
    ip = ip_lib.IPWrapper()
    name = common_utils.get_rand_device_name(prefix='vxlantest-')
    port = ip.add_vxlan(name, 3000)
    ip.del_veth(name)
    return name == port.name


def patch_supported():
    name, peer_name, patch_name = common_utils.get_related_rand_device_names(
        ['patchtest-', 'peertest0-', 'peertest1-'])
    with ovs_lib.OVSBridge(name) as br:
        port = br.add_patch_port(patch_name, peer_name)
        return port != ovs_lib.INVALID_OFPORT


def nova_notify_supported():
    try:
        # pylint:disable=import-outside-toplevel
        import neutron.notifiers.nova  # noqa since unused
        return True
    except ImportError:
        return False


def ofctl_arg_supported(cmd, **kwargs):
    """Verify if ovs-ofctl binary supports cmd with **kwargs.

    :param cmd: ovs-ofctl command to use for test.
    :param **kwargs: arguments to test with the command.
    :returns: a boolean if the supplied arguments are supported.
    """
    br_name = common_utils.get_rand_device_name(prefix='br-test-')
    with ovs_lib.OVSBridge(br_name) as test_br:
        full_args = ["ovs-ofctl", cmd, test_br.br_name,
                     ovs_lib._build_flow_expr_str(kwargs, cmd.split('-')[0],
                                                  False)]
        try:
            agent_utils.execute(full_args, run_as_root=True)
        except RuntimeError as e:
            LOG.debug("Exception while checking supported feature via "
                      "command %s. Exception: %s", full_args, e)
            return False
        except Exception:
            LOG.exception("Unexpected exception while checking supported"
                          " feature via command: %s", full_args)
            return False
        else:
            return True


def arp_responder_supported():
    mac = netaddr.EUI('dead:1234:beef', dialect=netaddr.mac_unix)
    ip = netaddr.IPAddress('240.0.0.1')
    actions = ovs_const.ARP_RESPONDER_ACTIONS % {'mac': mac, 'ip': ip}

    return ofctl_arg_supported(cmd='add-flow',
                               table=21,
                               priority=1,
                               proto='arp',
                               dl_vlan=42,
                               nw_dst='%s' % ip,
                               actions=actions)


def arp_header_match_supported():
    return ofctl_arg_supported(cmd='add-flow',
                               table=24,
                               priority=1,
                               proto='arp',
                               arp_op='0x2',
                               arp_spa='1.1.1.1',
                               actions="NORMAL")


def icmpv6_header_match_supported():
    return ofctl_arg_supported(cmd='add-flow',
                               table=ovs_const.ARP_SPOOF_TABLE,
                               priority=1,
                               dl_type=n_consts.ETHERTYPE_IPV6,
                               nw_proto=n_consts.PROTO_NUM_IPV6_ICMP,
                               icmp_type=n_consts.ICMPV6_TYPE_NA,
                               nd_target='fdf8:f53b:82e4::10',
                               actions="NORMAL")


def netns_read_requires_helper():
    nsname = "netnsreadtest-" + uuidutils.generate_uuid()
    ip_lib.create_network_namespace(nsname)
    try:
        # read without root_helper. if exists, not required.
        exists = ip_lib.network_namespace_exists(nsname)
    finally:
        ip_lib.delete_network_namespace(nsname)
    return not exists


def get_minimal_dnsmasq_version_supported():
    return MINIMUM_DNSMASQ_VERSION


def get_dnsmasq_version_with_dhcp_release6():
    return DNSMASQ_VERSION_DHCP_RELEASE6


def get_dnsmasq_version_with_host_addr6_list():
    return DNSMASQ_VERSION_HOST_ADDR6_LIST


def get_ovs_version_for_qos_direct_port_support():
    return DIRECT_PORT_QOS_MIN_OVS_VERSION


def dnsmasq_local_service_supported():
    cmd = ['dnsmasq', '--test', '--local-service']
    env = {'LC_ALL': 'C'}
    obj, cmd = agent_utils.create_process(cmd, addl_env=env)
    _stdout, _stderr = obj.communicate()
    returncode = obj.returncode
    if returncode == 127:
        LOG.debug("Exception while checking dnsmasq version. "
                  "dnsmasq: No such file or directory")
        return False
    elif returncode == 1:
        return False
    return True


def dnsmasq_version_supported():
    try:
        cmd = ['dnsmasq', '--version']
        env = {'LC_ALL': 'C'}
        out = agent_utils.execute(cmd, addl_env=env)
        m = re.search(r"version (\d+\.\d+)", out)
        ver = distutils.version.StrictVersion(m.group(1) if m else '0.0')
        if ver < distutils.version.StrictVersion(MINIMUM_DNSMASQ_VERSION):
            return False
        if (cfg.CONF.dnsmasq_enable_addr6_list is True and
                ver < distutils.version.StrictVersion(
                    DNSMASQ_VERSION_HOST_ADDR6_LIST)):
            LOG.warning('Support for multiple IPv6 addresses in host '
                        'entries was introduced in dnsmasq version '
                        '%(required)s. Found dnsmasq version %(current)s, '
                        'which does not support this feature. Unless support '
                        'for multiple IPv6 addresses was backported to the '
                        'running build of dnsmasq, the configuration option '
                        'dnsmasq_enable_addr6_list should be set to False.',
                        {'required': DNSMASQ_VERSION_HOST_ADDR6_LIST,
                         'current': ver})
    except (OSError, RuntimeError, IndexError, ValueError) as e:
        LOG.debug("Exception while checking minimal dnsmasq version. "
                  "Exception: %s", e)
        return False
    return True


def ovs_qos_direct_port_supported():
    try:
        cmd = ['ovs-vsctl', '-V']
        out = agent_utils.execute(cmd)
        matched_line = re.search(r"ovs-vsctl.*", out)
        matched_version = re.search(r"(\d+\.\d+)", matched_line.group(0))
        ver = versionutils.convert_version_to_tuple(matched_version.group(1) if
                                                    matched_version else '0.0')
        minver = versionutils.convert_version_to_tuple(
                 DIRECT_PORT_QOS_MIN_OVS_VERSION)
        if ver < minver:
            return False
    except (OSError, RuntimeError, ValueError) as e:
        LOG.debug("Exception while checking minimal ovs version "
                  "required for supporting direct ports QoS rules. "
                  "Exception: %s", e)
        return False
    return True


def dhcp_release6_supported():
    return priv_dhcp.dhcp_release6_supported()


def bridge_firewalling_enabled():
    for proto in ('arp', 'ip', 'ip6'):
        knob = 'net.bridge.bridge-nf-call-%stables' % proto
        cmd = ['sysctl', '-b', knob]
        try:
            out = agent_utils.execute(cmd)
        except (OSError, RuntimeError, IndexError, ValueError) as e:
            LOG.debug("Exception while extracting %(knob)s. "
                      "Exception: %(e)s", {'knob': knob, 'e': e})
            return False
        if out == '0':
            return False
    return True


class KeepalivedIPv6Test(object):
    def __init__(self, ha_port, gw_port, gw_vip, default_gw):
        l3_config.register_l3_agent_config_opts(l3_config.OPTS, cfg.CONF)
        self.ha_port = ha_port
        self.gw_port = gw_port
        self.gw_vip = gw_vip
        self.default_gw = default_gw
        self.manager = None
        self.config = None
        self.config_path = None
        self.nsname = "keepalivedtest-" + uuidutils.generate_uuid()
        self.pm = None
        self.orig_interval = cfg.CONF.AGENT.check_child_processes_interval

    def configure(self):
        config = keepalived.KeepalivedConf()
        instance1 = keepalived.KeepalivedInstance('MASTER', self.ha_port, 1,
                                                  ['169.254.192.0/18'],
                                                  advert_int=5)
        instance1.track_interfaces.append(self.ha_port)

        # Configure keepalived with an IPv6 address (gw_vip) on gw_port.
        vip_addr1 = keepalived.KeepalivedVipAddress(self.gw_vip, self.gw_port)
        instance1.vips.append(vip_addr1)

        # Configure keepalived with an IPv6 default route on gw_port.
        gateway_route = keepalived.KeepalivedVirtualRoute(n_consts.IPv6_ANY,
                                                          self.default_gw,
                                                          self.gw_port)
        instance1.virtual_routes.gateway_routes = [gateway_route]
        config.add_instance(instance1)
        self.config = config

    def start_keepalived_process(self):
        # Disable process monitoring for Keepalived process.
        cfg.CONF.set_override('check_child_processes_interval', 0, 'AGENT')
        self.pm = external_process.ProcessMonitor(cfg.CONF, 'router')

        # Create a temp directory to store keepalived configuration.
        self.config_path = tempfile.mkdtemp()

        # Instantiate keepalived manager with the IPv6 configuration.
        self.manager = keepalived.KeepalivedManager(
            'router1', self.config,
            namespace=self.nsname, process_monitor=self.pm,
            conf_path=self.config_path)
        self.manager.spawn()

    def verify_ipv6_address_assignment(self, gw_dev):
        process = self.manager.get_process()
        common_utils.wait_until_true(lambda: process.active)

        def _gw_vip_assigned():
            iface_ip = gw_dev.addr.list(ip_version=6, scope='global')
            if iface_ip:
                return self.gw_vip == iface_ip[0]['cidr']

        common_utils.wait_until_true(_gw_vip_assigned)

    def __enter__(self):
        ip_lib.create_network_namespace(self.nsname)
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        if self.pm:
            self.pm.stop()
        if self.manager:
            self.manager.disable()
        if self.config_path:
            shutil.rmtree(self.config_path, ignore_errors=True)
        ip_lib.delete_network_namespace(self.nsname)
        cfg.CONF.set_override('check_child_processes_interval',
                              self.orig_interval, 'AGENT')


def keepalived_ipv6_supported():
    """Check if keepalived supports IPv6 functionality.

    Validation is done as follows.
    1. Create a namespace.
    2. Create OVS bridge with two ports (ha_port and gw_port)
    3. Move the ovs ports to the namespace.
    4. Spawn keepalived process inside the namespace with IPv6 configuration.
    5. Verify if IPv6 address is assigned to gw_port.
    6. Verify if IPv6 default route is configured by keepalived.
    """

    br_name, ha_port, gw_port = common_utils.get_related_rand_device_names(
        ['ka-test-', ha_router.HA_DEV_PREFIX, namespaces.INTERNAL_DEV_PREFIX])
    gw_vip = 'fdf8:f53b:82e4::10/64'
    expected_default_gw = 'fe80:f816::1'

    with ovs_lib.OVSBridge(br_name) as br:
        with KeepalivedIPv6Test(ha_port, gw_port, gw_vip,
                                expected_default_gw) as ka:
            br.add_port(ha_port, ('type', 'internal'))
            br.add_port(gw_port, ('type', 'internal'))

            ha_dev = ip_lib.IPDevice(ha_port)
            gw_dev = ip_lib.IPDevice(gw_port)

            ha_dev.link.set_netns(ka.nsname)
            gw_dev.link.set_netns(ka.nsname)

            ha_dev.link.set_up()
            gw_dev.link.set_up()
            ha_dev.addr.add('169.254.192.8/18')

            ka.configure()

            ka.start_keepalived_process()

            ka.verify_ipv6_address_assignment(gw_dev)

            default_gw = gw_dev.route.get_gateway(ip_version=6)
            if default_gw:
                default_gw = default_gw['via']

    return expected_default_gw == default_gw


def ovsdb_native_supported():
    # Running the test should ensure we are configured for OVSDB native
    try:
        ovs = ovs_lib.BaseOVS()
        ovs.get_bridges()
        return True
    except ImportError as ex:
        LOG.error("Failed to import required modules. Ensure that the "
                  "python-openvswitch package is installed. Error: %s",
                  ex)
    except Exception:
        LOG.exception("Unexpected exception occurred.")

    return False


def ovs_conntrack_supported():
    br_name = common_utils.get_rand_device_name(prefix="ovs-test-")

    with ovs_lib.OVSBridge(br_name) as br:
        try:
            br.add_protocols(*["OpenFlow%d" % i for i in range(10, 15)])
        except RuntimeError as e:
            LOG.debug("Exception while checking ovs conntrack support: %s", e)
            return False
    return ofctl_arg_supported(cmd='add-flow', ct_state='+trk', actions='drop')


def ebtables_supported():
    try:
        cmd = ['ebtables', '--version']
        agent_utils.execute(cmd)
        return True
    except (OSError, RuntimeError, IndexError, ValueError) as e:
        LOG.debug("Exception while checking for installed ebtables. "
                  "Exception: %s", e)
        return False


def ipset_supported():
    try:
        cmd = ['ipset', '--version']
        agent_utils.execute(cmd)
        return True
    except (OSError, RuntimeError, IndexError, ValueError) as e:
        LOG.debug("Exception while checking for installed ipset. "
                  "Exception: %s", e)
        return False


def ip6tables_supported():
    try:
        cmd = ['ip6tables', '--version']
        agent_utils.execute(cmd)
        return True
    except (OSError, RuntimeError, IndexError, ValueError) as e:
        LOG.debug("Exception while checking for installed ip6tables. "
                  "Exception: %s", e)
        return False


def conntrack_supported():
    try:
        cmd = ['conntrack', '--version']
        agent_utils.execute(cmd)
        return True
    except (OSError, RuntimeError, IndexError, ValueError) as e:
        LOG.debug("Exception while checking for installed conntrack. "
                  "Exception: %s", e)
        return False


def get_minimal_dibbler_version_supported():
    return MINIMUM_DIBBLER_VERSION


def dibbler_version_supported():
    try:
        cmd = ['dibbler-client',
               'help']
        out = agent_utils.execute(cmd)
        return '-w' in out
    except (OSError, RuntimeError, IndexError, ValueError) as e:
        LOG.debug("Exception while checking minimal dibbler version. "
                  "Exception: %s", e)
        return False


def _fix_ip_nonlocal_bind_root_value(original_value):
    current_value = ip_lib.get_ip_nonlocal_bind(namespace=None)
    if current_value != original_value:
        ip_lib.set_ip_nonlocal_bind(value=original_value, namespace=None)


def ip_nonlocal_bind():
    nsname1 = "ipnonlocalbind1-" + uuidutils.generate_uuid()
    nsname2 = "ipnonlocalbind2-" + uuidutils.generate_uuid()

    ip_lib.create_network_namespace(nsname1)
    try:
        ip_lib.create_network_namespace(nsname2)
        try:
            original_value = ip_lib.get_ip_nonlocal_bind(namespace=None)
            try:
                ip_lib.set_ip_nonlocal_bind(value=0, namespace=nsname1)
                ip_lib.set_ip_nonlocal_bind(value=1, namespace=nsname2)
                ns1_value = ip_lib.get_ip_nonlocal_bind(namespace=nsname1)
            finally:
                _fix_ip_nonlocal_bind_root_value(original_value)
        except RuntimeError as e:
            LOG.debug("Exception while checking ip_nonlocal_bind. "
                      "Exception: %s", e)
            return False
        finally:
            ip_lib.delete_network_namespace(nsname2)
    finally:
        ip_lib.delete_network_namespace(nsname1)
    return ns1_value == 0


def gre_conntrack_supported():
    cmd = ['modinfo', CONNTRACK_GRE_MODULE]
    try:
        return agent_utils.execute(cmd, log_fail_as_error=False)
    except exceptions.ProcessExecutionError:
        return False


def min_tx_rate_support():
    device_mappings = helpers.parse_mappings(
        cfg.CONF.SRIOV_NIC.physical_device_mappings, unique_keys=False)
    devices_to_test = set()
    for devices_in_physnet in device_mappings.values():
        for device in devices_in_physnet:
            devices_to_test.add(device)

    # NOTE(ralonsoh): the VF used by default is 0. Each SR-IOV configured
    # NIC should have configured at least 1 VF.
    VF_NUM = 0
    devices_without_support = set()
    for device in devices_to_test:
        try:
            ip_link = ip_lib.IpLinkCommand(device)
            # NOTE(ralonsoh): to set min_tx_rate, first is needed to set
            # max_tx_rate and max_tx_rate >= min_tx_rate.
            vf_config = {'vf': VF_NUM, 'rate': {'min_tx_rate': int(400),
                                                'max_tx_rate': int(500)}}
            ip_link.set_vf_feature(vf_config)
            vf_config = {'vf': VF_NUM, 'rate': {'min_tx_rate': 0,
                                                'max_tx_rate': 0}}
            ip_link.set_vf_feature(vf_config)
        except ip_lib.InvalidArgument:
            devices_without_support.add(device)

    if devices_without_support:
        LOG.debug('The following NICs do not support "min_tx_rate": %s',
                  devices_without_support)
        return False
    return True
