#!/usr/bin/python
#
# addNetwork: add a vdsm-controlled network to this host.
#
# network topology is one of:
#
# br --------------------- nic
#
# br  --- v  ------------- nic
#                        /
# br2 --- v2 ------------
#
# br ----------- bond --- nic1
#                     \
#                      -- nic2
#
# br  --- v  --- bond --- nic1
#              /      \
# br2 --- v2 --        -- nic2

import sys, subprocess, os, shutil
import re
from pipes import quote as pq
import constants
import netinfo, utils

import neterrors as ne

NET_CONF_PREF = utils.NET_CONF_DIR + 'ifcfg-'
PROC_NET_BONDING = '/proc/net/bonding'
CONFFILE_HEADER = '# automatically generated by vdsm'
DELETED_HEADER = '# original file did not exist'

def usage():
    print """Usage:

%s bridge {vlan-id|''} {bonding|''} nic[,nic] [option=value]

Add a network defined by a bridge, tagged by vlan-id,
connected through bonding device to nics.

bridge - network name
vlan-id - integer 0-4095 or empty string if no vlan
bonding - bonding device name or empty string if a single nic
nic[,nic] - possibly-multiple nics
""" % (sys.argv[0])
    sys.exit(ne.ERR_BAD_PARAMS)

def conffileBackup(filename):
    import pwd
    try:
        os.mkdir(utils.NET_CONF_BACK_DIR)
	os.chown(utils.NET_CONF_BACK_DIR, pwd.getpwnam('vdsm').pw_uid, 0)
    except:
        pass
    if os.path.exists('/usr/libexec/ovirt-functions'):
        subprocess.call([constants.EXT_SH, '/usr/libexec/ovirt-functions',
                         'unmount_config', filename])
    (dummy, basename) = os.path.split(filename)
    backup = os.path.join(utils.NET_CONF_BACK_DIR, basename)
    if os.path.exists(backup):
        # original copy already backed up
        return
    if os.path.exists(filename):
        shutil.copy2(filename, backup)
    else:
        file(backup, 'w').write(DELETED_HEADER + '\n')
    os.chown(backup, pwd.getpwnam('vdsm').pw_uid, 0)

def addBridge(name, GATEWAYDEV=None, IPADDR=None, NETMASK=None, GATEWAY=None,
        BOOTPROTO=None, DELAY='0', ONBOOT='yes', **kwargs):
    s = """DEVICE=%s
TYPE=Bridge
ONBOOT=%s
""" % (pq(name), pq(ONBOOT))
    if IPADDR:
        s = s + 'IPADDR=%s\nNETMASK=%s\n' % (pq(IPADDR), pq(NETMASK))
        if GATEWAY:
            s = s + 'GATEWAY=%s\n' % pq(GATEWAY)
    else:
        if BOOTPROTO:
            s = s + 'BOOTPROTO=%s\n' % pq(BOOTPROTO)
    s += 'DELAY=%s\n' % pq(DELAY)
    BLACKLIST = ['TYPE', 'NAME', 'DEVICE', 'BONDING_OPTS',
                 'force', 'blockingdhcp',
                 'connectivityCheck', 'connectivityTimeout']
    for k in set(kwargs.keys()).difference(set(BLACKLIST)):
        if re.match('^[a-zA-Z_]\w*$', k):
            s += '%s=%s\n' % (k, pq(kwargs[k]))
        else:
            print 'ignoreing variable %s' % k
    conffile = NET_CONF_PREF + name
    conffileBackup(conffile)
    file(conffile, 'w').write(s)
    os.chmod(conffile, 0664)

def addVlan(vlanId, iface, bridge):
    conffile = NET_CONF_PREF + iface + '.' + vlanId
    conffileBackup(conffile)
    file(conffile, 'w').write("""DEVICE=%s.%s
ONBOOT=yes
VLAN=yes
BOOTPROTO=none
BRIDGE=%s
""" % (pq(iface), vlanId, pq(bridge)))
    os.chmod(conffile, 0664)

def addBonding(bonding, bridge=None):
    conffile = NET_CONF_PREF + bonding
    conffileBackup(conffile)
    f = file(conffile, 'w')
    f.write("""DEVICE=%s
ONBOOT=yes
BOOTPROTO=none
""" % (bonding))
    if bridge:
        f.write('BRIDGE=%s\n' % pq(bridge))
    bopts = options.get('BONDING_OPTS', '')
    if not bopts:
        bopts = 'mode=802.3ad miimon=150'
    f.write('BONDING_OPTS=%s' % pq(bopts))
    f.close()
    os.chmod(conffile, 0664)
    # create the bonding device to avoid initscripts noise
    if bonding not in file('/sys/class/net/bonding_masters').read().split():
        file('/sys/class/net/bonding_masters', 'w').write('+%s\n' % bonding)

def addNic(nic, bonding=None, bridge=None):
    conffile = NET_CONF_PREF + nic
    conffileBackup(conffile)
    hwaddr = _netinfo['nics'][nic].get('permhwaddr') or \
             _netinfo['nics'][nic]['hwaddr']
    f = file(conffile, 'w')
    f.write('DEVICE=%s\nONBOOT=yes\nBOOTPROTO=none\nHWADDR=%s\n' % (pq(nic),
            pq(hwaddr)))
    if bridge:
        f.write('BRIDGE=%s\n' % pq(bridge))
    if bonding:
        f.write('MASTER=%s\n' % pq(bonding))
        f.write('SLAVE=yes\n')
    f.close()
    os.chmod(conffile, 0664)

def ifdown(iface):
    p = subprocess.Popen([constants.EXT_IFDOWN, iface], stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE, close_fds=True)
    out, err = p.communicate()
    sys.stdout.write(out)
    sys.stderr.write('\n'.join([line for line in err.splitlines()
                                if not line.endswith(' does not exist!')]))
    return p.returncode

def ipcalc(checkopt, s):
    if not isinstance(s, basestring):
        return False
    p = subprocess.Popen([constants.EXT_IPCALC, '-c', checkopt, s],
            close_fds=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            stderr=subprocess.PIPE)
    out, err = p.communicate()
    if err:
        print >> sys.stderr, err
    return not p.returncode

def assertIPADDR(IPADDR=None, NETMASK=None, GATEWAY=None, **kwargs):
    if (IPADDR or  NETMASK or  GATEWAY) and not (
        IPADDR and NETMASK):
        print 'incomplete static IP specification'
        sys.exit(ne.ERR_BAD_ADDR)
    if IPADDR:
        if not ipcalc('-4', IPADDR):
            sys.exit(ne.ERR_BAD_ADDR)
        if not ipcalc('-m', NETMASK):
            sys.exit(ne.ERR_BAD_ADDR)
        if GATEWAY:
            if not ipcalc('-4', GATEWAY):
                sys.exit(ne.ERR_BAD_ADDR)

def assertNics(nics):
    for nic in nics:
        if nic not in _netinfo['nics']:
            print 'addNetwork: unknown nic %s' % (nic)
            sys.exit(ne.ERR_BAD_NIC)
    for bridge, netdict in _netinfo['networks'].iteritems():
        for iface in netdict['ports']:
            for nic in nics:
                if nic == iface:
                    print 'addNetwork: nic %s already bound to bridge %s' % (
                            nic, bridge)
                    sys.exit(ne.ERR_USED_NIC)

def assertNicsFreeOfVlans(vlan, bonding, nics):
    if not bonding and vlan:
        return
    for nic in nics:
        for v, vdict in _netinfo['vlans'].iteritems():
            if nic == vdict['iface']:
                print 'addNetwork: nic %s already used by vlan %s' % (
                        nics, v)
                sys.exit(ne.ERR_USED_NIC)

def assertMultipleNicsBond(bonding, nics):
    if not bonding and len(nics) > 1:
        print 'addNetwork: multiple nics require bonding device'
        sys.exit(ne.ERR_BAD_BONDING)

def assertSameOrNewBond(bonding, nics):
    if not bonding:
        for b, bdict in _netinfo['bondings'].iteritems():
            if nics and nics[0] in bdict['slaves']:
                print 'addNetwork: %s already enslaved to %s' % (nics, b)
                sys.exit(ne.ERR_USED_NIC)
        return

    try:
        ensnics = _netinfo["bondings"][bonding]["slaves"]
    except KeyError:
        ensnics = []
    if ensnics:
        if set(nics) == set(ensnics):
            return
        print 'delNetwork: %s are not all nics enslaved to %s %s' % (
                nics, bonding, ensnics)
        sys.exit(ne.ERR_BAD_BONDING)
    for bnd, bonddict in _netinfo['bondings'].iteritems():
        if bnd == bonding:
            continue
        isect = set(nics).intersection(set(bonddict['slaves']))
        if isect:
             print 'addNetwork: nic %s already enslaved to %s' % (
                         ','.join(isect), bnd)
             sys.exit(ne.ERR_USED_NIC)

def assertBridge(bridge):
    if bridge in _netinfo['networks'].keys():
        print 'addNetwork: network %s already defined' % (bridge)
        sys.exit(ne.ERR_USED_BRIDGE)
    if not bridge or len(bridge) > 15 or \
            set(bridge).intersection(set([':', '.', ' ', '\t'])):
        print 'addNetwork: bad network name %s' % (bridge,)
        sys.exit(ne.ERR_BAD_BRIDGE)

def assertBonding(vlan, bonding):
    if not bonding:
        return

    if not re.match('^bond[0-9]+$', bonding):
        print 'addNetwork: %s is not a valid bonding device name' % bonding
        sys.exit(ne.ERR_BAD_BONDING)

    for bridge, netdict in _netinfo['networks'].iteritems():
        for iface in netdict['ports']:
            if iface == bonding or \
               (vlan and iface == bonding + '.' + vlan) or \
               (not vlan and iface.startswith(bonding + '.')):
                    print 'addNetwork: bonding %s already member of %s' % (
                        iface, bridge)
                    sys.exit(ne.ERR_BAD_BONDING)

def assertVlan(vlan):
    if not vlan:
        return
    try:
        if not 0 <= int(vlan) <= 4095:
            raise ValueError
    except:
        print 'addNetwork: \'%s\' not a valid vlan id' % (vlan)
        sys.exit(ne.ERR_BAD_VLAN)

def assertNoBondVlanIfNoNic(vlan, bonding, nics):
    if not nics:
        if vlan or bonding:
            print 'addNetwork: no nics: must not define vlan or bonding'
            sys.exit(ne.ERR_BAD_PARAMS)

if len(sys.argv) <= 4:
    usage()

bridge, vlan, bonding, nics = sys.argv[1:5]
if nics == '':
    nics = []
else:
    nics = nics.split(',')

options = {}
for arg in sys.argv[5:]:
    k, v = arg.split('=', 1)
    options[k] = v

if len(nics) > 1 and not bonding:
    usage()

_netinfo = netinfo.get()
if not utils.tobool(options.get('force')):
    assertBridge(bridge)
    assertIPADDR(**options)
    assertNics(nics)
    assertBonding(vlan, bonding)
    assertMultipleNicsBond(bonding, nics)
    assertSameOrNewBond(bonding, nics)
    assertVlan(vlan)
    assertNoBondVlanIfNoNic(vlan, bonding, nics)
    assertNicsFreeOfVlans(vlan, bonding, nics)

addBridge(bridge, **options)
ifaceBridge = bridge
if vlan:
    addVlan(vlan, bonding or nics[0], ifaceBridge)
    # since we have vlan device, it is connected to the bridge. other
    # interfaces should be connected to the bridge through vlan, and not
    # directly.
    ifaceBridge = None

if bonding:
    addBonding(bonding, ifaceBridge)
    for nic in nics:
        addNic(nic, bonding=bonding)
else:
    for nic in nics:
        addNic(nic, bridge=ifaceBridge)

# take down nics that need to be changed
vlanedIfaces = [v['iface'] for v in _netinfo['vlans'].values()]
if bonding not in vlanedIfaces:
    for nic in nics:
        if nic not in vlanedIfaces:
            ifdown(nic)
ifdown(bridge)
for nic in nics:
    subprocess.call([constants.EXT_IFUP, nic])
if bonding:
    subprocess.call([constants.EXT_IFUP, bonding])
if vlan:
    subprocess.call([constants.EXT_IFUP, (bonding or nics[0]) + '.' + vlan])
if options.get('BOOTPROTO') == 'dhcp' and \
   not utils.tobool(options.get('blockingdhcp')):
    # wait for dhcp in another process
    sys.stdout.close()
    sys.stderr.close()
    utils.createDaemon()
subprocess.call([constants.EXT_IFUP, bridge])
