#!/usr/bin/env python

# The goal with this script is put in the exports xml file everything
# that can be parsed from the /etc/exports file. If there are problems
# parsing the file, warnings are printed, but parsing of the rest of
# the file continues.

import sys, os, time
from isi.ui.shell.util import errx, warnx
from isi.config import nfs
from syslog import syslog, LOG_WARNING, LOG_ERR

# Globals
just_testing = False
override_file = "/ifs/.ifsvar/modules/nfs/nfs_override.xml"
backup_file = None

class State:
    pass

def sys_errx(msg):
    global just_testing, backup_file, override_file
    if not just_testing:
        syslog(LOG_ERR, 'isi_exports_update: ' + msg)
        # Backup file is not needed as original didn't change if we
        # have an error.
        if backup_file:
            try:
                os.rename(backup_file, override_file)
            except:
                sys_warnx("Can't restore backup override file %s to %s" % \
                          (backup_file, override_file))
    # errx() exits
    errx(msg)

def sys_warnx(msg):
    global just_testing
    if not just_testing:
        syslog(LOG_WARNING, 'isi_exports_update: ' + msg)
    warnx(msg)

def parsecred(export, arg):
    parts = arg.split(':')
    export.map_user = parts[0]
    export.map_groups = [p for p in parts[1:] if p != '']
    export.map_nogroups = arg[-1] == ':'
    export.options_mask1 |= nfs.NFS_EXPORT_OM1_MAP_GROUPS
    export.options_mask1 |= nfs.NFS_EXPORT_OM1_MAP_NOGROUPS
    export.options_mask1 |= nfs.NFS_EXPORT_OM1_MAP_USER

def do_opt(export, state, opt, arg, file_name, line_num):
    if opt in ['ro', 'o']:
        export.read_only = True
        export.options_mask1 |= nfs.NFS_EXPORT_OM1_READ_ONLY
    elif opt == 'alldirs':
        export.all_dirs = True
        export.options_mask1 |= nfs.NFS_EXPORT_OM1_ALL_DIRS
    elif opt in ['maproot', 'root', 'r']:
        export.map_type = nfs.NFS_EXPORT_MAP_ROOT
        parsecred(export, arg)
        state.usedarg = True
        export.options_mask1 |= nfs.NFS_EXPORT_OM1_MAP_TYPE
    elif opt == 'mapall':
        export.map_type = nfs.NFS_EXPORT_MAP_ALL
        parsecred(export, arg)
        state.usedarg = True
        export.options_mask1 |= nfs.NFS_EXPORT_OM1_MAP_TYPE
    elif opt in ['network', 'n']:
        if state.net:
            sys_warnx('-network option seen twice (%s:%d)' % \
                      (file_name, line_num))
            return False
        state.net = arg
        state.usedarg = True
    elif opt in ['mask', 'm']:
        state.mask = arg
        state.usedarg = True
    elif opt in ['quiet']: # Options that we may see but ignore should be here
        sys_warnx('-%s ignored (%s:%d)' % (opt, file_name, line_num))
    else:
        sys_warnx('Bad option %s (%s:%d)' % (opt, file_name, line_num))
        return False

    return True

# If at least one valid directory with valid options on the line
# return the export object. Otherwise, return None.
def parse_line(line, file_name, line_num):
    got_nondir = False

    state = State()
    state.net = None
    state.mask = None
    state.usedarg = False
    export = nfs.Export()

    words = line.split()
    for i in range(len(words)):
        if state.usedarg: # Handle previous word having used us as an arg
            state.usedarg = False
            continue
        word, nextword = words[i], i < len(words) - 1 and words[i + 1] or None
        if word[0] == '/':
            if got_nondir:
                sys_warnx('Directories must be first on line (%s:%d)' % \
                          (file_name, line_num))
                return None
            if not os.path.isdir(word):
                sys_warnx("Invalid path %s (%s:%d)" % \
                      (word, file_name, line_num))
                # Other directories on line might be OK
                continue
            export.paths.append(word)
        elif word[0] == '-':
            # This handles '-opt', '-opt arg', and '-opt1=arg1,opt2,opt3 arg3'
            got_nondir = True
            opts = word[1:].split(',')
            for j in range(len(opts)):
                opt, arg, index = opts[j], None, 0
                if opt.__contains__('='):
                    index = opt.find('=')
                    opt, arg = opt[:index], opt[index + 1:]
                elif j == len(opts) - 1:
                    arg = nextword
                if not do_opt(export, state, opt, arg, file_name, line_num):
                    return None
                if j != len(opts) - 1 or index: # Can only affect word level
                    state.usedarg = False
        else:
            got_nondir = True
            export.clients.append(nfs.Client(word))

    if len(export.paths) == 0:
        sys_warnx("No valid directories listed on line (%s:%d)" % (file_name, line_num))
        return None

    # Put together client
    if state.net and not state.mask:
        export.clients.append(nfs.Client(state.net))
    elif state.net and state.mask:
        export.clients.append(nfs.Client('%s/%s' % (state.net, state.mask)))
    elif state.mask:
        sys_warnx('Mask provided with no network (%s:%d)' % \
                  (file_name, line_num))
        return None
    
    return export

def main(argv):
    global just_testing, backup_file, override_file
    if not 2 <= len(argv) <= 3:
        errx('usage: <exports file> [-test]')

    if len(argv) == 3:
        if argv[2] != '-test':
            errx('usage: <exports file> [-test]')
        just_testing = True

    file_name = argv[1]
    exports = []
    try:
        f = open(file_name)
    except:
        sys_errx("Can't open NFS exports file %s" % file_name)
        
    if os.path.exists(override_file):
        backup_file = "%s.BACKUP.%s" % (override_file, time.strftime("%Y%m%d%H%M%S"))
        try:
            os.rename(override_file, backup_file)
        except:
            sys_warnx("Unable to backup override to %s" % backup_file)
            backup_file = None

    line_num = 0
    for line in f.readlines():
        line_num += 1
        # Strip comments and whitespace
        line = line[:line.find('#')]
        line.strip()
        if len(line) == 0:
            continue
        
        export = parse_line(line, file_name, line_num)
        if export != None:
            exports.append([export, line_num])

    if len(exports) == 0:
        sys_errx('No valid exports defined in %s' % file_name)

    exp = nfs.Exports()
    exp.export_set(1, None)
    bad_count = 0
    for (export, line_num) in exports:
        try:
            exp.export_set(nfs.NFS_EXPORT_ID_NEW, export, True, True)
        except Exception, e:
            if str(e).find('bad host') != -1:
                try:
                    # Try again, but this time allow bad hosts as we
                    # don't want to lose export settings of currently
                    # unresolvable hosts on upgrade.
                    exp.export_set(nfs.NFS_EXPORT_ID_NEW, export, True)
                except Exception, e:
                    bad_count += 1
            else:
                bad_count += 1
            sys_warnx('%s (%s:%d)' % \
                  (str(e), file_name, line_num))
    if bad_count == len(exports):
        sys_errx('No lines in %s have been accepted' % file_name)
    if not just_testing:
        exp.commit()

if __name__ == "__main__":
        main(sys.argv[:])
