#!/usr/bin/env python3
#
# Copyright (c) 2008,2009 Citrix Systems, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; version 2.1 only. with the special
# exception on linking described in file LICENSE.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
"""Usage:

    %(command-name)s <PIF> up
    %(command-name)s <PIF> down
    %(command-name)s rewrite
    %(command-name)s --force <BRIDGE> up
    %(command-name)s --force <BRIDGE> down
    %(command-name)s --force <BRIDGE> rewrite --device=<INTERFACE> --mac=<MAC-ADDRESS> <CONFIG>

    where <PIF> is one of:
       --session <SESSION-REF> --pif <PIF-REF>
       --pif-uuid <PIF-UUID>
    and <CONFIG> is one of:
       --mode=dhcp
       --mode=static --ip=<IPADDR> --netmask=<NM> [--gateway=<GW>]

  Options:
    --session           A session reference to use to access the xapi DB
    --pif               A PIF reference within the session.
    --pif-uuid          The UUID of a PIF.
    --force             An interface name.
    --root-prefix=DIR   Use DIR as alternate root directory (for testing).
    --no-syslog         Write log messages to stderr instead of system log.
"""

# Notes:
# 1. Every pif belongs to exactly one network
# 2. Every network has zero or one pifs
# 3. A network may have an associated bridge, allowing vifs to be attached
# 4. A network may be bridgeless (there's no point having a bridge over a storage pif)

from InterfaceReconfigure import *

import os, sys, getopt
import syslog
import traceback
import re
import random
import syslog

management_pif = None

dbcache_file = "/var/xapi/network.dbcache"

#
# Logging.
#

def log_pif_action(action, pif):
    pifrec = db().get_pif_record(pif)
    rec = {}
    rec['uuid'] = pifrec['uuid']
    rec['ip_configuration_mode'] = pifrec['ip_configuration_mode']
    rec['action'] = action
    rec['pif_netdev_name'] = pif_netdev_name(pif)
    rec['message'] = "Bring %(action)s PIF %(uuid)s" % rec
    log("%(message)s: %(pif_netdev_name)s configured as %(ip_configuration_mode)s" % rec)

#
# Exceptions.
#

class Usage(Exception):
    def __init__(self, msg):
        Exception.__init__(self)
        self.msg = msg

#
# Boot from Network filesystem or device.
#

def check_allowed(pif):
    """Determine whether interface-reconfigure should be manipulating this PIF.

    Used to prevent system PIFs (such as network root disk) from being interfered with.
    """

    pifrec = db().get_pif_record(pif)
    try:
        f = open(root_prefix() + "/proc/ardence")
        macline = filter(lambda x: x.startswith("HWaddr:"), f.readlines())
        f.close()
        if len(macline) == 1:
            p = re.compile(".*\s%(MAC)s\s.*" % pifrec, re.IGNORECASE)
            if p.match(macline[0]):
                log("Skipping PVS device %(device)s (%(MAC)s)" % pifrec)
                return False
    except IOError:
        pass
    return True

#
# Bare Network Devices -- network devices without IP configuration
#

def netdev_remap_name(pif, already_renamed=[]):
    """Check whether 'pif' exists and has the correct MAC.
    If not, try to find a device with the correct MAC and rename it.
    'already_renamed' is used to avoid infinite recursion.
    """

    def read1(name):
        file = None
        try:
            file = open(name, 'r')
            return file.readline().rstrip('\n')
        finally:
            if file != None:
                file.close()

    def get_netdev_mac(device):
        try:
            return read1("%s/sys/class/net/%s/address" % (root_prefix(), device))
        except:
            # Probably no such device.
            return None

    def get_netdev_tx_queue_len(device):
        try:
            return int(read1("%s/sys/class/net/%s/tx_queue_len" % (root_prefix(), device)))
        except:
            # Probably no such device.
            return None

    def get_netdev_by_mac(mac):
        for device in os.listdir(root_prefix() + "/sys/class/net"):
            dev_mac = get_netdev_mac(device)
            if (dev_mac and mac.lower() == dev_mac.lower() and
                get_netdev_tx_queue_len(device)):
                return device
        return None

    def rename_netdev(old_name, new_name):
        raise Error("Trying to rename %s to %s - This functionality has been removed" % (old_name, new_name))
        # log("Changing the name of %s to %s" % (old_name, new_name))
        # run_command(['/sbin/ip', 'link', 'set', old_name, 'down'])
        # if not run_command(['/sbin/ip', 'link', 'set', old_name, 'name', new_name]):
        #     raise Error("Could not rename %s to %s" % (old_name, new_name))

    pifrec = db().get_pif_record(pif)
    device = pifrec['device']
    mac = pifrec['MAC']

    # Is there a network device named 'device' at all?
    device_exists = netdev_exists(device)
    if device_exists:
        # Yes.  Does it have MAC 'mac'?
        found_mac = get_netdev_mac(device)
        if found_mac and mac.lower() == found_mac.lower():
            # Yes, everything checks out the way we want.  Nothing to do.
            return
    else:
        log("No network device %s" % device)

    # What device has MAC 'mac'?
    cur_device = get_netdev_by_mac(mac)
    if not cur_device:
        log("No network device has MAC %s" % mac)
        return

    # First rename 'device', if it exists, to get it out of the way
    # for 'cur_device' to replace it.
    if device_exists:
        rename_netdev(device, "dev%d" % random.getrandbits(24))

    # Rename 'cur_device' to 'device'.
    rename_netdev(cur_device, device)

#
# IP Network Devices -- network devices with IP configuration
#

def ifdown(netdev):
    """Bring down a network interface"""
    if not netdev_exists(netdev):
        log("ifdown: device %s does not exist, ignoring" % netdev)
        return
    if not os.path.exists("%s/etc/sysconfig/network-scripts/ifcfg-%s" % (root_prefix(), netdev)):
        log("ifdown: device %s exists but ifcfg-%s does not" % (netdev,netdev))
        run_command(["/sbin/ip", "link", "set", netdev, 'down'])
        return
    run_command(["/sbin/ifdown", netdev])

def ifup(netdev):
    """Bring up a network interface"""
    if not os.path.exists(root_prefix() + "/etc/sysconfig/network-scripts/ifcfg-%s" % netdev):
        raise Error("ifup: device %s exists but ifcfg-%s does not" % (netdev,netdev))
    d = os.getenv("DHCLIENTARGS","")
    if os.path.exists("/etc/firstboot.d/data/firstboot_in_progress"):
        os.putenv("DHCLIENTARGS", d + " -T 240 " )
    run_command(["/sbin/ifup", netdev])
    os.putenv("DHCLIENTARGS", d )

#
#
#

def pif_rename_physical_devices(pif):
    if pif_is_tunnel(pif):
        return

    if pif_is_vlan(pif):
        pif = pif_get_vlan_slave(pif)

    if pif_is_bond(pif):
        pifs = pif_get_bond_slaves(pif)
    else:
        pifs = [pif]

    for pif in pifs:
        netdev_remap_name(pif)

#
# IP device configuration
#

def ipdev_configure_static_routes(interface, oc, f):
    """Open a route-<interface> file for static routes.

    Opens the static routes configuration file for interface and writes one
    line for each route specified in the network's other config "static-routes" value.
    E.g. if
           interface ( RO): xenbr1
           other-config (MRW): static-routes: 172.16.0.0/15/192.168.0.3,172.18.0.0/16/192.168.0.4;...

    Then route-xenbr1 should be
          172.16.0.0/15 via 192.168.0.3 dev xenbr1
          172.18.0.0/16 via 192.168.0.4 dev xenbr1
    """
    if 'static-routes' in oc:
        # The key is present - extract comma separates entries
        lines = oc['static-routes'].split(',')
    else:
        # The key is not present, i.e. there are no static routes
        lines = []

    child = ConfigurationFile("%s/etc/sysconfig/network-scripts/route-%s" % (root_prefix(), interface))
    child.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \
            (os.path.basename(child.path()), os.path.basename(sys.argv[0])))

    try:
        for l in lines:
            network, masklen, gateway = l.split('/')
            child.write("%s/%s via %s dev %s\n" % (network, masklen, gateway, interface))

        f.attach_child(child)
        child.close()

    except ValueError as e:
        log("Error in other-config['static-routes'] format for network %s: %s" % (interface, e))

def ipdev_open_ifcfg(pif):
    ipdev = pif_ipdev_name(pif)

    log("Writing network configuration for %s" % ipdev)

    f = ConfigurationFile("%s/etc/sysconfig/network-scripts/ifcfg-%s" % (root_prefix(), ipdev))

    f.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \
            (os.path.basename(f.path()), os.path.basename(sys.argv[0])))
    f.write("XEMANAGED=yes\n")
    f.write("DEVICE=%s\n" % ipdev)
    f.write("ONBOOT=no\n")
    f.write("NOZEROCONF=yes\n")

    return f

def ipdev_configure_network(pif, dp):
    """Write the configuration file for a network.

    Writes configuration derived from the network object into the relevant
    ifcfg file.  The configuration file is passed in, but if the network is
    bridgeless it will be ifcfg-<interface>, otherwise it will be ifcfg-<bridge>.

    This routine may also write ifcfg files of the networks corresponding to other PIFs
    in order to maintain consistency.

    params:
        pif:  Opaque_ref of pif
        dp:   Datapath object
    """

    pifrec = db().get_pif_record(pif)
    nw = pifrec['network']
    nwrec = db().get_network_record(nw)

    ipdev = pif_ipdev_name(pif)

    f = ipdev_open_ifcfg(pif)

    mode = pifrec['ip_configuration_mode']
    log("Configuring %s using %s configuration" % (ipdev, mode))

    oc = None
    if 'other_config' in pifrec:
        oc = pifrec['other_config']

    dp.configure_ipdev(f)

    if pifrec['ip_configuration_mode'] == "DHCP":
        f.write("BOOTPROTO=dhcp\n")
        f.write("PERSISTENT_DHCLIENT=yes\n")
    elif pifrec['ip_configuration_mode'] == "Static":
        f.write("BOOTPROTO=none\n")
        f.write("NETMASK=%(netmask)s\n" % pifrec)
        f.write("IPADDR=%(IP)s\n" % pifrec)
        f.write("GATEWAY=%(gateway)s\n" % pifrec)
    elif pifrec['ip_configuration_mode'] == "None":
        f.write("BOOTPROTO=none\n")
    else:
        raise Error("Unknown ip-configuration-mode %s" % pifrec['ip_configuration_mode'])

    if 'other_config' in nwrec:
        settings,offload = ethtool_settings(nwrec['other_config'])
        if len(settings):
            f.write("ETHTOOL_OPTS=\"%s\"\n" % str.join(" ", settings))
        if len(offload):
            f.write("ETHTOOL_OFFLOAD_OPTS=\"%s\"\n" % str.join(" ", offload))

        ipdev_configure_static_routes(ipdev, nwrec['other_config'], f)

    mtu = mtu_setting(nw, "Network", nwrec['other_config'])
    if mtu:
        f.write("MTU=%s\n" % mtu)


    if 'DNS' in pifrec and pifrec['DNS'] != "":
        ServerList = pifrec['DNS'].split(",")
        for i in range(len(ServerList)): f.write("DNS%d=%s\n" % (i+1, ServerList[i]))
    if oc and 'domain' in oc:
        f.write("DOMAIN='%s'\n" % oc['domain'].replace(',', ' '))

    # There can be only one DNSDEV and one GATEWAYDEV in /etc/sysconfig/network.
    #
    # The peerdns pif will be the one with
    # pif::other-config:peerdns=true, or the mgmt pif if none have
    # this set.
    #
    # The gateway pif will be the one with
    # pif::other-config:defaultroute=true, or the mgmt pif if none
    # have this set.

    # Work out which pif on this host should be the DNSDEV and which
    # should be the GATEWAYDEV
    #
    # Note: we prune out the bond master pif (if it exists). This is
    # because when we are called to bring up an interface with a bond
    # master, it is implicit that we should bring down that master.

    pifs_on_host = [p for p in db().get_all_pifs() if not p in pif_get_bond_masters(pif)]

    # now prune out bond slaves as they are not connected to the IP 
    # stack and so cannot be used as gateway or DNS devices.
    pifs_on_host = [ p for p in pifs_on_host if len(pif_get_bond_masters(p)) == 0]

    # loop through all the pifs on this host looking for one with
    #   other-config:peerdns = true, and one with
    #   other-config:default-route=true
    peerdns_pif = None
    defaultroute_pif = None
    for __pif in pifs_on_host:
        __pifrec = db().get_pif_record(__pif)
        __oc = __pifrec['other_config']
        if 'peerdns' in __oc and __oc['peerdns'] == 'true':
            if peerdns_pif == None:
                peerdns_pif = __pif
            else:
                log('Warning: multiple pifs with "peerdns=true" - choosing %s and ignoring %s' % \
                        (db().get_pif_record(peerdns_pif)['device'], __pifrec['device']))
        if 'defaultroute' in __oc and __oc['defaultroute'] == 'true':
            if defaultroute_pif == None:
                defaultroute_pif = __pif
            else:
                log('Warning: multiple pifs with "defaultroute=true" - choosing %s and ignoring %s' % \
                        (db().get_pif_record(defaultroute_pif)['device'], __pifrec['device']))

    # If no pif is explicitly specified then use the mgmt pif for
    # peerdns/defaultroute.
    if peerdns_pif == None:
        peerdns_pif = management_pif
    if defaultroute_pif == None:
        defaultroute_pif = management_pif

    is_dnsdev = peerdns_pif == pif
    is_gatewaydev = defaultroute_pif == pif

    if is_dnsdev or is_gatewaydev:
        fnetwork = ConfigurationFile(root_prefix() + "/etc/sysconfig/network")
        for line in fnetwork.readlines():
            if is_dnsdev and line.lstrip().startswith('DNSDEV='):
                fnetwork.write('DNSDEV=%s\n' % ipdev)
                is_dnsdev = False
            elif is_gatewaydev and line.lstrip().startswith('GATEWAYDEV='):
                fnetwork.write('GATEWAYDEV=%s\n' % ipdev)
                is_gatewaydev = False
            else:
                fnetwork.write(line)

        if is_dnsdev:
            fnetwork.write('DNSDEV=%s\n' % ipdev)
        if is_gatewaydev:
            fnetwork.write('GATEWAYDEV=%s\n' % ipdev)

        fnetwork.close()
        f.attach_child(fnetwork)

    return f

#
# Toplevel actions
#

def action_up(pif, force):
    pifrec = db().get_pif_record(pif)

    ipdev = pif_ipdev_name(pif)
    dp = DatapathFactory()(pif)

    log("action_up: %s" % ipdev)

    f = ipdev_configure_network(pif, dp)

    dp.preconfigure(f)

    f.close()

    pif_rename_physical_devices(pif)

    # if we are not forcing the interface up then attempt to tear down
    # any existing devices which might interfere with brinign this one
    # up.
    if not force:
        ifdown(ipdev)

        dp.bring_down_existing()

    try:
        f.apply()

        dp.configure()

        ifup(ipdev)

        dp.post()

        # Update /etc/issue (which contains the IP address of the management interface)
        os.system(root_prefix() + "/sbin/update-issue")

        f.commit()
    except Error as e:
        log("failed to apply changes: %s" % e.msg)
        f.revert()
        raise

def action_down(pif):
    ipdev = pif_ipdev_name(pif)
    dp = DatapathFactory()(pif)

    log("action_down: %s" % ipdev)

    ifdown(ipdev)

    dp.bring_down()

def action_rewrite():
    DatapathFactory().rewrite()
    
# This is useful for reconfiguring the mgmt interface after having lost connectivity to the pool master
def action_force_rewrite(bridge, config):
    def getUUID():
        import subprocess
        uuid,_ = subprocess.Popen(['uuidgen'], stdout = subprocess.PIPE).communicate()
        return uuid.strip()

    # Notes:
    # 1. that this assumes the interface is bridged
    # 2. If --gateway is given it will make that the default gateway for the host

    # extract the configuration
    try:
        mode = config['mode']
        mac = config['mac']
        interface = config['device']
    except:
        raise Usage("Please supply --mode, --mac and --device")

    if mode == 'static':
        try:
            netmask = config['netmask']
            ip = config['ip']
        except:
            raise Usage("Please supply --netmask and --ip")
        try:
            gateway = config['gateway']
        except:
            gateway = None
    elif mode != 'dhcp':
        raise Usage("--mode must be either static or dhcp")

    if 'vlan' in config:
        is_vlan = True
        vlan_slave, vlan_vid = config['vlan'].split('.')
    else:
        is_vlan = False

    if is_vlan:
        raise Error("Force rewrite of VLAN not implemented")

    log("Configuring %s using %s configuration" % (bridge, mode))

    f = ConfigurationFile(root_prefix() + dbcache_file)

    pif_uuid = getUUID()
    network_uuid = getUUID()

    f.write('<?xml version="1.0" ?>\n')
    f.write('<xenserver-network-configuration>\n')
    f.write('\t<pif ref="OpaqueRef:%s">\n' % pif_uuid)
    f.write('\t\t<network>OpaqueRef:%s</network>\n' % network_uuid)
    f.write('\t\t<management>True</management>\n')
    f.write('\t\t<uuid>%sPif</uuid>\n' % interface)
    f.write('\t\t<bond_slave_of>OpaqueRef:NULL</bond_slave_of>\n')
    f.write('\t\t<bond_master_of/>\n')
    f.write('\t\t<VLAN_slave_of/>\n')
    f.write('\t\t<VLAN_master_of>OpaqueRef:NULL</VLAN_master_of>\n')
    f.write('\t\t<VLAN>-1</VLAN>\n')
    f.write('\t\t<tunnel_access_PIF_of/>\n')
    f.write('\t\t<tunnel_transport_PIF_of/>\n')
    f.write('\t\t<device>%s</device>\n' % interface)
    f.write('\t\t<MAC>%s</MAC>\n' % mac)
    f.write('\t\t<other_config/>\n')
    if mode == 'dhcp':
        f.write('\t\t<ip_configuration_mode>DHCP</ip_configuration_mode>\n')
        f.write('\t\t<IP></IP>\n')
        f.write('\t\t<netmask></netmask>\n')
        f.write('\t\t<gateway></gateway>\n')
        f.write('\t\t<DNS></DNS>\n')
    elif mode == 'static':
        f.write('\t\t<ip_configuration_mode>Static</ip_configuration_mode>\n')
        f.write('\t\t<IP>%s</IP>\n' % ip)
        f.write('\t\t<netmask>%s</netmask>\n' % netmask)
        if gateway is not None:
            f.write('\t\t<gateway>%s</gateway>\n' % gateway)
        f.write('\t\t<DNS></DNS>\n')
    else:
        raise Error("Unknown mode %s" % mode)
    f.write('\t</pif>\n')

    f.write('\t<network ref="OpaqueRef:%s">\n' % network_uuid)
    f.write('\t\t<uuid>InitialManagementNetwork</uuid>\n')
    f.write('\t\t<PIFs>\n')
    f.write('\t\t\t<PIF>OpaqueRef:%s</PIF>\n' % pif_uuid)
    f.write('\t\t</PIFs>\n')
    f.write('\t\t<bridge>%s</bridge>\n' % bridge)
    f.write('\t\t<other_config/>\n')
    f.write('\t</network>\n')
    f.write('</xenserver-network-configuration>\n')

    f.close()

    try:
        f.apply()
        f.commit()
    except Error as e:
        log("failed to apply changes: %s" % e.msg)
        f.revert()
        raise

def main(argv=None):
    global management_pif

    session = None
    pif_uuid = None
    pif = None

    force_interface = None
    force_management = False

    if argv is None:
        argv = sys.argv

    try:
        try:
            shortops = "h"
            longops = [ "pif=", "pif-uuid=",
                        "session=",
                        "force=",
                        "force-interface=",
                        "management",
                        "mac=", "device=", "mode=", "ip=", "netmask=", "gateway=",
                        "root-prefix=",
                        "no-syslog",
                        "help" ]
            arglist, args = getopt.gnu_getopt(argv[1:], shortops, longops)
        except getopt.GetoptError as msg:
            raise Usage(msg)

        force_rewrite_config = {}

        for o,a in arglist:
            if o == "--pif":
                pif = a
            elif o == "--pif-uuid":
                pif_uuid = a
            elif o == "--session":
                session = a
            elif o == "--force-interface" or o == "--force":
                force_interface = a
            elif o == "--management":
                force_management = True
            elif o in ["--mac", "--device", "--mode", "--ip", "--netmask", "--gateway"]:
                force_rewrite_config[o[2:]] = a
            elif o == "--root-prefix":
                set_root_prefix(a)
            elif o == "--no-syslog":
                set_log_destination("stderr")
            elif o == "-h" or o == "--help":
                print(__doc__ % {'command-name': os.path.basename(argv[0])})
                return 0

        if get_log_destination() == "syslog":
            syslog.openlog(os.path.basename(argv[0]))
            log("Called as " + str.join(" ", argv))

        if len(args) < 1:
            raise Usage("Required option <action> not present")
        if len(args) > 1:
            raise Usage("Too many arguments")

        action = args[0]

        if not action in ["up", "down", "rewrite", "rewrite-configuration"]:
            raise Usage("Unknown action \"%s\"" % action)

        # backwards compatibility
        if action == "rewrite-configuration": action = "rewrite"

        if ( session or pif ) and pif_uuid:
            raise Usage("--session/--pif and --pif-uuid are mutually exclusive.")
        if ( session and not pif ) or ( not session and pif ):
            raise Usage("--session and --pif must be used together.")
        if force_interface and ( session or pif or pif_uuid ):
            raise Usage("--force is mutually exclusive with --session, --pif and --pif-uuid")
        if len(force_rewrite_config) and not (force_interface and action == "rewrite"):
            raise Usage("\"--force rewrite\" needed for --device, --mode, --ip, --netmask, and --gateway")
        if (action == "rewrite") and (pif or pif_uuid ):
            raise Usage("rewrite action does not take --pif or --pif-uuid")
        
        global db
        if force_interface:
            log("Force interface %s %s" % (force_interface, action))

            if action == "rewrite":
                action_force_rewrite(force_interface, force_rewrite_config)
            elif action in ["up", "down"]:
                db_init_from_cache(dbcache_file)
                pif = db().get_pif_by_bridge(force_interface)
                management_pif = db().get_management_pif()

                if action == "up":
                    action_up(pif, True)
                elif action == "down":
                    action_down(pif)
            else:
                raise Error("Unknown action %s"  % action)
        else:
            db_init_from_xenapi(session)

            if pif_uuid:
                pif = db().get_pif_by_uuid(pif_uuid)

            if action == "rewrite":
                action_rewrite()
            else:
                if not pif:
                    raise Usage("No PIF given")

                if force_management:
                    # pif is going to be the management pif
                    management_pif = pif
                else:
                    # pif is not going to be the management pif.
                    # Search DB cache for pif on same host with management=true
                    pifrec = db().get_pif_record(pif)
                    management_pif = db().get_management_pif()

                log_pif_action(action, pif)

                if not check_allowed(pif):
                    return 0

                if action == "up":
                    action_up(pif, False)
                elif action == "down":
                    action_down(pif)
                else:
                    raise Error("Unknown action %s"  % action)

            # Save cache.
            db().save(dbcache_file)

    except Usage as err:
        sys.stderr.write(err.msg + "\n")
        sys.stderr.write("For help use --help.\n")
        sys.stderr.flush()
        return 2
    except Error as err:
        log(err.msg)
        return 1

    return 0

if __name__ == "__main__":
    rc = 1
    try:
        rc = main()
    except:
        ex = sys.exc_info()
        err = traceback.format_exception(*ex)
        for exline in err:
            log(exline)

    syslog.closelog()

    sys.exit(rc)
