#!/usr/bin/python3
#
# linuxmuster-import-devices
# thomas@linuxmuster.net
# 20210609
#

import configparser
import constants
import os
import re
import subprocess
import sys
import time
import getopt

from os import listdir
from os.path import isfile, join
from pathlib import Path

from functions import getBootImage, getDevicesArray, getGrubOstype, getGrubPart
from functions import getSetupValue, getStartconfOsValues, getStartconfOption
from functions import getStartconfPartnr, getStartconfPartlabel, getSubnetArray
from functions import isValidHostIpv4, printScript, readTextfile
from functions import setGlobalStartconfOption, writeTextfile

def usage():
    print('Usage: linuxmuster-import-devices [options]')
    print(' [options] may be:')
    print(' -s <schoolname>,   --school=<schoolname>   : Select a school other than default-school.')

# read commandline arguments
# get cli args
try:
    opts, args = getopt.getopt(sys.argv[1:], "s:", ["school="])
except getopt.GetoptError as err:
    # print help information and exit:
    print(err) # will print something like "option -a not recognized"
    usage()
    sys.exit(2)

# default valued
school = 'default-school'

# evaluate options
for o, a in opts:
    if o in ("-s", "--school"):
        school = a

# default school's devices.csv
devices = constants.WIMPORTDATA

# read INIFILE
setup = configparser.ConfigParser(delimiters=('='), inline_comment_prefixes=('#', ';'))
setup.read(constants.SETUPINI)
serverip = setup.get('setup', 'serverip')
opsiip = setup.get('setup', 'opsiip')
domainname = setup.get('setup', 'domainname')

# start message
printScript(os.path.basename(__file__), 'begin')

# do sophomorix-devices first
msg = 'Starting sophomorix-device syntax check:'
printScript(msg)
result = subprocess.check_call('sophomorix-device --dry-run', shell=True)
msg = 'sophomorix-device finished '
if result != 0:
    printScript(msg + ' errors detected!')
    sys.exit(result)

printScript(msg + ' OK!')
subprocess.call('sophomorix-device --sync', shell=True)


# functions begin
# write grub cfgs
def doGrubCfg(startconf, group, kopts):
    grubcfg = constants.LINBOGRUBDIR + '/' + group + '.cfg'
    rc, content = readTextfile(grubcfg)
    if rc and constants.MANAGEDSTR not in content:
        return 'present'
    # get grub partition name of cache
    cache = getStartconfOption(startconf, 'LINBO', 'Cache')
    partnr = getStartconfPartnr(startconf, cache)
    systemtype = getStartconfOption(startconf, 'LINBO', 'SystemType')
    cacheroot = getGrubPart(cache, systemtype)
    cachelabel = getStartconfPartlabel(startconf, partnr)
    # if cache is not defined provide a forced netboot cfg
    if cacheroot is None:
        netboottpl = constants.LINBOTPLDIR + '/grub.cfg.forced_netboot'
        subprocess.call('cp ' + netboottpl + ' ' + grubcfg, shell=True)
        return 'not yet configured!'
    # create return message
    if os.path.isfile(grubcfg):
        msg = 'replaced'
    else:
        msg = 'created'
    # create gobal part for group cfg
    globaltpl = constants.LINBOTPLDIR + '/grub.cfg.global'
    rc, content = readTextfile(globaltpl)
    if not rc:
        return 'error!'
    replace_list = [('@@group@@', group), ('@@cachelabel@@', cachelabel), ('@@cacheroot@@', cacheroot), ('@@kopts@@', kopts)]
    for item in replace_list:
        content = content.replace(item[0], item[1])
    rc = writeTextfile(grubcfg, content, 'w')
    # get os infos from group's start.conf
    oslists = getStartconfOsValues(startconf)
    if oslists is None:
        return 'error!'
    # write os parts to grub cfg
    ostpl = constants.LINBOTPLDIR + '/grub.cfg.os'
    for oslist in oslists:
        osname, partition, kernel, initrd, kappend, osnr = oslist
        osroot = getGrubPart(partition, systemtype)
        ostype = getGrubOstype(osname)
        partnr = getStartconfPartnr(startconf, partition)
        oslabel = getStartconfPartlabel(startconf, partnr)
        # add root to kernel append
        if 'root=' not in kappend:
            kappend = kappend + ' root=' + partition
        rc, content = readTextfile(ostpl)
        if not rc:
            return 'error!'
        replace_list = [('@@group@@', group), ('@@cachelabel@@', cachelabel),
            ('@@cacheroot@@', cacheroot), ('@@osname@@', osname),
            ('@@osnr@@', osnr), ('@@ostype@@', ostype), ('@@oslabel@@', oslabel),
            ('@@osroot@@', osroot), ('@@partnr@@', partnr), ('@@kernel@@', kernel),
            ('@@initrd@@', initrd), ('@@kopts@@', kopts), ('@@append@@', kappend)]
        for item in replace_list:
            content = content.replace(item[0], str(item[1]))
        rc = writeTextfile(grubcfg, content, 'a')
        if not rc:
            return 'error!'
    return msg


# write linbo start configuration file
def doLinboStartconf(group):
    startconf = constants.LINBODIR + '/start.conf.' + group
    # provide unconfigured start.conf if there is none for this group
    if os.path.isfile(startconf):
        if getStartconfOption(startconf, 'LINBO', 'Cache') is None:
            msg1 = 'not yet configured!'
        else:
            msg1 = 'present'
    else:
        msg1 = 'not yet configured!'
        subprocess.call('cp ' + constants.LINBODIR + '/start.conf ' + startconf, shell=True)
    # read values from start.conf
    group_s = getStartconfOption(startconf, 'LINBO', 'Group')
    serverip_s = getStartconfOption(startconf, 'LINBO', 'Server')
    kopts_s = getStartconfOption(startconf, 'LINBO', 'KernelOptions')
    # get alternative server ip from kernel options
    try:
        serverip_k = re.findall(r'server=[^ ]*', kopts_s, re.IGNORECASE)[0].split('=')[1]
    except Exception as error:
        # print(error)
        serverip_k = None
    # determine whether global values from start conf have to changed
    if serverip_k is not None and isValidHostIpv4(serverip_k):
        serverip_r = serverip_k
    else:
        serverip_r = serverip
    if kopts_s is None:
        kopts_r = 'splash quiet'
    else:
        kopts_r = kopts_s
    if group_s != group:
        group_r = group
    else:
        group_r = group
    # change global startconf options if necessary
    if serverip_s != serverip_r:
        rc = setGlobalStartconfOption(startconf, 'Server', serverip_r)
        if not rc:
            return rc
    if kopts_s != kopts_r:
        rc = setGlobalStartconfOption(startconf, 'KernelOptions', kopts_r)
        if not rc:
            return rc
    if group_s != group_r:
        rc = setGlobalStartconfOption(startconf, 'Group', group_r)
        if not rc:
            return rc
    # process grub cfgs
    msg2 = doGrubCfg(startconf, group, kopts_r)
    # format row in columns for output
    row = [group, msg1, msg2]
    printScript("  {: <15} | {: <20} | {: <20}".format(*row))


# opsi
def doOpsi():
    printScript('', 'begin')
    printScript('Working on opsi integration')
    sshcmd = 'ssh -oNumberOfPasswordPrompts=0 -oStrictHostKeyChecking=no'
    rsynccmd = 'rsync -e "' + sshcmd + '"'
    # upload workstations file
    subprocess.call(rsynccmd + ' ' + constants.WIMPORTDATA + ' ' + opsiip + ':' + constants.OPSIWSDATA, shell=True)
    # execute script
    subprocess.call(sshcmd + ' ' + opsiip + ' ' + constants.OPSIWSIMPORT + ' --quiet', shell=True)
    # download opsi host keys
    subprocess.call(rsynccmd + ' ' + opsiip + ':' + constants.OPSIPCKEYS + ' ' + constants.LINBOOPSIKEYS, shell=True)
    subprocess.call('chmod 600 ' + constants.LINBOOPSIKEYS, shell=True)


# write dhcp subnet devices config
def writeDhcpDevicesConfig(school='default-school'):
    printScript('', 'begin')
    printScript('Working on dhcp configuration for devices')
    opsiip = getSetupValue('opsiip')
    host_decl_tpl = """host @@hostname@@ {
  option host-name "@@hostname@@";
  hardware ethernet @@mac@@;
"""
    baseConfigFilePath = constants.DHCPDEVCONF
    devicesConfigBasedir = "/etc/dhcp/devices"
    Path(devicesConfigBasedir).mkdir(parents=True, exist_ok=True)

    cfgfile = devicesConfigBasedir + "/" + school + ".conf"
    if os.path.isfile(cfgfile):
        os.unlink(cfgfile)
    if os.path.isfile(baseConfigFilePath):
        os.unlink(baseConfigFilePath)
    try:
        # open devices/<school>.conf for append
        with open(cfgfile, 'a') as outfile:
            # iterate over the defined subnets
            subnets = getSubnetArray('0')
            subnets.append(['DHCP'])
            for item in subnets:
                subnet = item[0]
                # iterate over devices per subnet
                headline = False
                for device_array in getDevicesArray(fieldnrs='1,2,3,4,7,8,10',subnet=subnet,stype=True, school=school):
                    if not headline:
                        # write corresponding subnet as a comment
                        if subnet == 'DHCP':
                            outfile.write('# dynamic ip hosts\n')
                            printScript('* dynamic ip hosts:')
                        else:
                            outfile.write('# subnet ' + subnet + '\n')
                            printScript('* in subnet ' + subnet + ':')
                        headline = True
                    hostname, group, mac, ip, dhcpopts, computertype, pxeflag, systemtype = device_array
                    if systemtype is None:
                        systemtype = ''
                    if len(computertype) > 15:
                        computertype = computertype[0:15]
                    # format row in columns for output
                    row = [hostname, ip, computertype, pxeflag, systemtype]
                    printScript("  {: <15} | {: <15} | {: <15} | {: <1} | {: <6}".format(*row))
                    # begin host declaration
                    host_decl = host_decl_tpl.replace('@@mac@@', mac).replace('@@hostname@@', hostname)
                    # fixed ip
                    if ip != 'DHCP':
                        host_decl = host_decl + '  fixed-address ' + ip + ';\n'
                    # only for pxe clients
                    if int(pxeflag) != 0:
                        # get grub bootimage dependend to group's systemtype in start.conf
                        bootimage = getBootImage(systemtype)
                        # opsi pxe boot
                        if pxeflag == '3':
                            if bootimage is not None and 'efi' in bootimage:
                                bootimage = constants.OPSIEFIPXEFILE
                            else:
                                bootimage = constants.OPSIPXEFILE
                            host_decl = host_decl + '  filename "' + bootimage + '";\n'
                            host_decl = host_decl + '  next-server ' + opsiip + ';\n'
                        # linbo pxe boot
                        else:
                            host_decl = host_decl + '  option extensions-path "' + group + '";\n'
                            if 'filename' not in dhcpopts and bootimage is not None:
                                host_decl = host_decl + '  filename "boot/grub/' + bootimage + '";\n'
                            # dhcp options have to be 5 chars minimum to get processed
                            if len(dhcpopts) > 4:
                                for opt in dhcpopts.split(','):
                                    host_decl = host_decl + '  ' + opt + ';\n'
                    # finish host declaration
                    host_decl = host_decl + '}\n'
                    # finallý write host declaration
                    outfile.write(host_decl)

        # open devices.conf for append
        with open(baseConfigFilePath, 'a') as outfile:
            for devicesConf in listdir(devicesConfigBasedir):
                outfile.write("include \"{0}/{1}\";\n".format(devicesConfigBasedir, devicesConf))

    except Exception as error:
        print(error)
        return False
# functions end


# delete old config links
# start.conf-ip
subprocess.call('find ' + constants.LINBODIR + ' -name start.conf-\* -type l -exec rm {} \;', shell=True)
subprocess.call('find ' + constants.LINBOGRUBDIR + '/hostcfg -name \*.cfg -type l -exec rm {} \;', shell=True)


# write dhcp devices.conf
writeDhcpDevicesConfig(school=school)


# linbo stuff
printScript('', 'begin')
printScript('Working on linbo/grub configuration for devices:')
pxe_groups = []
for device_array in getDevicesArray(fieldnrs='1,2,3,4,10', subnet='all', pxeflag='1,2,3', school=school):
    host, group, mac, ip, pxeflag = device_array
    # collect groups with pxe for later use
    if group not in pxe_groups:
        pxe_groups.append(group)
    # format row in columns for output
    row = [host, group]
    printScript("  {: <15} | {: <15}".format(*row))
    groupconf = 'start.conf.' + group
    hostlink = constants.LINBODIR + '/start.conf-'
    if ip == 'DHCP':
        hostlink = hostlink + mac.lower()
    else:
        hostlink = hostlink + ip
    if not os.path.isfile(hostlink):
        subprocess.call('ln -sf ' + groupconf + ' ' + hostlink, shell=True)
    # grub config
    groupconf = '../' + group + '.cfg'
    hostlink = constants.LINBOGRUBDIR + '/hostcfg/' + host + '.cfg'
    if not os.path.isfile(hostlink):
        subprocess.call('ln -sf ' + groupconf + ' ' + hostlink, shell=True)


# write pxe configs for collected groups
printScript('', 'begin')
printScript('Working on linbo/grub configuration for groups:')
printScript("  {: <15} | {: <20} | {: <20}".format(*[' ', 'linbo start.conf', 'grub cfg']))
printScript("  {: <15}+{: <20}+{: <20}".format(*['-'*16, '-'*22, '-'*21]))
for group in pxe_groups:
    doLinboStartconf(group)


# do opsi stuff if configured
if opsiip != '':
    doOpsi()


# execute post hooks
hookpath = constants.POSTDEVIMPORT
hookscripts = [f for f in listdir(hookpath) if isfile(join(hookpath, f)) and os.access(join(hookpath, f), os.X_OK)]
if len(hookscripts) > 0:
    printScript('', 'begin')
    printScript('Executing post hooks:')
    for h in hookscripts:
        hookscript = hookpath + '/' + h
        msg = '* ' + h + ' '
        printScript(msg, '', False, False, True)
        output = subprocess.getoutput(hookscript)
        if output != '':
            print(output)

# restart services
printScript('', 'begin')
printScript('Restarting services:')
service = 'isc-dhcp-server'
subprocess.call('service ' + service + ' restart', shell=True)
# wait one second before service check
time.sleep(1)
rc = 0
msg = '* ' + service + ' '
printScript(msg, '', False, False, True)
state = subprocess.check_call('systemctl is-active --quiet ' + service, shell=True)
if state == 0:
    printScript(' OK!', '', True, True, False, len(msg))
else:
    printScript(' Failed!', '', True, True, False, len(msg))
    rc = 1

# end message
printScript(os.path.basename(__file__), 'end')

sys.exit(rc)
