# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
Implements vlans, bridges, and iptables rules using linux utilities.
"""

import logging
import os
import signal

# TODO(ja): does the definition of network_path belong here?

from nova import db
from nova import flags
from nova import utils


def _bin_file(script):
    """Return the absolute path to scipt in the bin directory"""
    return os.path.abspath(os.path.join(__file__, "../../../bin", script))


FLAGS = flags.FLAGS
flags.DEFINE_string('dhcpbridge_flagfile',
                    '/etc/nova/nova-dhcpbridge.conf',
                    'location of flagfile for dhcpbridge')

flags.DEFINE_string('networks_path', '$state_path/networks',
                    'Location to keep network config files')
flags.DEFINE_string('public_interface', 'vlan1',
                    'Interface for public IP addresses')
flags.DEFINE_string('vlan_interface', 'eth0',
                    'network device for vlans')
flags.DEFINE_string('dhcpbridge', _bin_file('nova-dhcpbridge'),
                        'location of nova-dhcpbridge')
flags.DEFINE_string('cc_host', utils.get_my_ip(), 'ip of api server')
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
flags.DEFINE_string('routing_source_ip', '127.0.0.1',
                    'Public IP of network host')
flags.DEFINE_bool('use_nova_chains', False,
                  'use the nova_ routing chains instead of default')

DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)]


def metadata_forward():
    """Create forwarding rule for metadata"""
    _confirm_rule("PREROUTING", "-t nat -s 0.0.0.0/0 "
             "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT "
             "--to-destination %s:%s" % (FLAGS.cc_host, FLAGS.cc_port))


def init_host():
    """Basic networking setup goes here"""
    # NOTE(devcamcar): Cloud public SNAT entries and the default
    # SNAT rule for outbound traffic.
    _confirm_rule("POSTROUTING", "-t nat -s %s "
             "-j SNAT --to-source %s"
             % (FLAGS.fixed_range, FLAGS.routing_source_ip))

    _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" %
                  FLAGS.fixed_range)
    _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" %
                  {'range': FLAGS.fixed_range})


def bind_floating_ip(floating_ip):
    """Bind ip to public interface"""
    _execute("sudo ip addr add %s dev %s" % (floating_ip,
                                             FLAGS.public_interface))


def unbind_floating_ip(floating_ip):
    """Unbind a public ip from public interface"""
    _execute("sudo ip addr del %s dev %s" % (floating_ip,
                                             FLAGS.public_interface))


def ensure_vlan_forward(public_ip, port, private_ip):
    """Sets up forwarding rules for vlan"""
    _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" %
                  private_ip)
    _confirm_rule("PREROUTING",
                  "-t nat -d %s -p udp --dport %s -j DNAT --to %s:1194"
            % (public_ip, port, private_ip))


def ensure_floating_forward(floating_ip, fixed_ip):
    """Ensure floating ip forwarding rule"""
    _confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
                           % (floating_ip, fixed_ip))
    _confirm_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s"
                           % (fixed_ip, floating_ip))
    # TODO(joshua): Get these from the secgroup datastore entries
    _confirm_rule("FORWARD", "-d %s -p icmp -j ACCEPT"
                           % (fixed_ip))
    for (protocol, port) in DEFAULT_PORTS:
        _confirm_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT"
            % (fixed_ip, protocol, port))


def remove_floating_forward(floating_ip, fixed_ip):
    """Remove forwarding for floating ip"""
    _remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
                          % (floating_ip, fixed_ip))
    _remove_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s"
                          % (fixed_ip, floating_ip))
    _remove_rule("FORWARD", "-d %s -p icmp -j ACCEPT"
                          % (fixed_ip))
    for (protocol, port) in DEFAULT_PORTS:
        _remove_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT"
                              % (fixed_ip, protocol, port))


def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
    """Create a vlan and bridge unless they already exist"""
    interface = ensure_vlan(vlan_num)
    ensure_bridge(bridge, interface, net_attrs)


def ensure_vlan(vlan_num):
    """Create a vlan unless it already exists"""
    interface = "vlan%s" % vlan_num
    if not _device_exists(interface):
        logging.debug("Starting VLAN inteface %s", interface)
        _execute("sudo vconfig set_name_type VLAN_PLUS_VID_NO_PAD")
        _execute("sudo vconfig add %s %s" % (FLAGS.vlan_interface, vlan_num))
        _execute("sudo ifconfig %s up" % interface)
    return interface


def ensure_bridge(bridge, interface, net_attrs=None):
    """Create a bridge unless it already exists"""
    if not _device_exists(bridge):
        logging.debug("Starting Bridge interface for %s", interface)
        _execute("sudo brctl addbr %s" % bridge)
        _execute("sudo brctl setfd %s 0" % bridge)
        # _execute("sudo brctl setageing %s 10" % bridge)
        _execute("sudo brctl stp %s off" % bridge)
        if interface:
            _execute("sudo brctl addif %s %s" % (bridge, interface))
    if net_attrs:
        _execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \
                (bridge,
                 net_attrs['gateway'],
                 net_attrs['broadcast'],
                 net_attrs['netmask']))
    else:
        _execute("sudo ifconfig %s up" % bridge)
    _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge)
    _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge)


def get_dhcp_hosts(context, network_id):
    """Get a string containing a network's hosts config in dnsmasq format"""
    hosts = []
    for fixed_ip_ref in db.network_get_associated_fixed_ips(context,
                                                            network_id):
        hosts.append(_host_dhcp(fixed_ip_ref))
    return '\n'.join(hosts)


# NOTE(ja): Sending a HUP only reloads the hostfile, so any
#           configuration options (like dchp-range, vlan, ...)
#           aren't reloaded.
def update_dhcp(context, network_id):
    """(Re)starts a dnsmasq server for a given network

    if a dnsmasq instance is already running then send a HUP
    signal causing it to reload, otherwise spawn a new instance
    """
    network_ref = db.network_get(context, network_id)

    conffile = _dhcp_file(network_ref['bridge'], 'conf')
    with open(conffile, 'w') as f:
        f.write(get_dhcp_hosts(context, network_id))

    # Make sure dnsmasq can actually read it (it setuid()s to "nobody")
    os.chmod(conffile, 0644)

    pid = _dnsmasq_pid_for(network_ref['bridge'])

    # if dnsmasq is already running, then tell it to reload
    if pid:
        out, _err = _execute('cat /proc/%d/cmdline' % pid,
                             check_exit_code=False)
        if conffile in out:
            try:
                _execute('sudo kill -HUP %d' % pid)
                return
            except Exception as exc:  # pylint: disable-msg=W0703
                logging.debug("Hupping dnsmasq threw %s", exc)
        else:
            logging.debug("Pid %d is stale, relaunching dnsmasq", pid)

    # FLAGFILE and DNSMASQ_INTERFACE in env
    env = {'FLAGFILE': FLAGS.dhcpbridge_flagfile,
           'DNSMASQ_INTERFACE': network_ref['bridge']}
    command = _dnsmasq_cmd(network_ref)
    _execute(command, addl_env=env)


def _host_dhcp(fixed_ip_ref):
    """Return a host string for an address"""
    instance_ref = fixed_ip_ref['instance']
    return "%s,%s.novalocal,%s" % (instance_ref['mac_address'],
                                   instance_ref['hostname'],
                                   fixed_ip_ref['address'])


def _execute(cmd, *args, **kwargs):
    """Wrapper around utils._execute for fake_network"""
    if FLAGS.fake_network:
        logging.debug("FAKE NET: %s", cmd)
        return "fake", 0
    else:
        return utils.execute(cmd, *args, **kwargs)


def _device_exists(device):
    """Check if ethernet device exists"""
    (_out, err) = _execute("ifconfig %s" % device, check_exit_code=False)
    return not err


def _confirm_rule(chain, cmd):
    """Delete and re-add iptables rule"""
    if FLAGS.use_nova_chains:
        chain = "nova_%s" % chain.lower()
    _execute("sudo iptables --delete %s %s" % (chain, cmd),
             check_exit_code=False)
    _execute("sudo iptables -I %s %s" % (chain, cmd))


def _remove_rule(chain, cmd):
    """Remove iptables rule"""
    if FLAGS.use_nova_chains:
        chain = "%s" % chain.lower()
    _execute("sudo iptables --delete %s %s" % (chain, cmd))


def _dnsmasq_cmd(net):
    """Builds dnsmasq command"""
    cmd = ['sudo -E dnsmasq',
           ' --strict-order',
           ' --bind-interfaces',
           ' --conf-file=',
           ' --pid-file=%s' % _dhcp_file(net['bridge'], 'pid'),
           ' --listen-address=%s' % net['gateway'],
           ' --except-interface=lo',
           ' --dhcp-range=%s,static,120s' % net['dhcp_start'],
           ' --dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'),
           ' --dhcp-script=%s' % FLAGS.dhcpbridge,
           ' --leasefile-ro']
    return ''.join(cmd)


def _stop_dnsmasq(network):
    """Stops the dnsmasq instance for a given network"""
    pid = _dnsmasq_pid_for(network)

    if pid:
        try:
            _execute('sudo kill -TERM %d' % pid)
        except Exception as exc:  # pylint: disable-msg=W0703
            logging.debug("Killing dnsmasq threw %s", exc)


def _dhcp_file(bridge, kind):
    """Return path to a pid, leases or conf file for a bridge"""

    if not os.path.exists(FLAGS.networks_path):
        os.makedirs(FLAGS.networks_path)
    return os.path.abspath("%s/nova-%s.%s" % (FLAGS.networks_path,
                                              bridge,
                                              kind))


def _dnsmasq_pid_for(bridge):
    """Returns the pid for prior dnsmasq instance for a bridge

    Returns None if no pid file exists

    If machine has rebooted pid might be incorrect (caller should check)
    """

    pid_file = _dhcp_file(bridge, 'pid')

    if os.path.exists(pid_file):
        with open(pid_file, 'r') as f:
            return int(f.read())
