#!/usr/bin/python3
#
# lmn-prepare
# thomas@linuxmuster.net
# 20260622
#

import argparse
import configparser
import datetime
import getpass
import netifaces
import os
import pathlib
import pwd
import re
import socket
import subprocess
import sys

from dataclasses import dataclass
from shutil import copyfile
from typing import Optional

from IPy import IP


# default values
hostname = None
hostip = None
firewallip = None
gateway = None
ipnet = None
ipnet_default = '10.0.0.'
bitmask_default = '16'
serverip = ''
iface = None
iface_default = None
network = None
bitmask = None
netmask = None
broadcast = None
domainname = 'linuxmuster.lan'
swapsize = '2'
pkgs = None
ipnr = None
rootpw_default = 'Muster!'
rootpw = 'Muster!'
profile_list = ['server', 'ubuntu']
user_list = ['linuxmuster', 'linuxadmin', 'lmn', 'lmnadmin']
profile = None
profile_ini = None
unattended = False
initial = False
setup = False
reboot = False
force = False
updates = False
iniread = False
createcert = False
nokeys = False
nopw = False
setup_mode = None
sharedir = '/usr/share/linuxmuster/prepare'
templates = sharedir + '/templates'
libdir = '/var/lib/linuxmuster'
prepini = libdir + '/prepare.ini'
setupini = libdir + '/setup.ini'
purgepkgs = 'cloud-guest-utils cloud-init cloud-init-base lxc snapd'
srvpkgs = 'linuxmuster-base7 linuxmuster-linbo7 linuxmuster-webui7 linuxmuster-linbo-gui7 sophomorix-samba'
swapfile = '/swap.img'


@dataclass
class PrepareConfig:
    profile: Optional[str] = None
    hostname: Optional[str] = None
    ipnet: Optional[str] = None
    firewallip: Optional[str] = None
    gateway: Optional[str] = None
    domainname: str = domainname
    swapsize: str = swapsize
    rootpw: str = rootpw_default
    serverip: str = serverip
    initial: bool = False
    setup: bool = False
    unattended: bool = False
    force: bool = False
    createcert: bool = False
    nokeys: bool = False
    nopw: bool = False
    reboot: bool = False
    setup_mode: Optional[str] = None


def run_shell_command(cmd, shell=False, check=False, capture_output=False):
    proc = subprocess.run(cmd, shell=shell, check=check, capture_output=capture_output, text=True)
    return proc if capture_output else proc.returncode


## functions start

# help
def usage(rc):
    print('Usage: lmn-prepare [options]')
    print('\n [options] are:\n')
    print('-x, --force                 : Force run on an already configured system.')
    print('-i, --initial               : Prepare the appliance initially for rollout.')
    print('-s, --setup                 : Further appliance setup (network, swapsize).')
    print('                              Note: You have to use either -i or -s.')
    print('-t, --hostname=<hostname>   : Hostname to apply (optional, works only with')
    print('                              server profile).')
    print('-n, --ipnet=<ip/bitmask>    : Ip address and bitmask assigned to the host')
    print('                              (optional, default is 10.0.0.x/16, depending')
    print('                              on the profile).')
    print('-p, --profile=<profile>     : Host profile to apply, mandatory. Expected')
    print('                              values are "server" or "ubuntu".')
    print('                              Profile name is also used as hostname, except for')
    print('                              "server" if set with -t.')
    print('-c, --createcert            : Create self signed server cert (to be used only')
    print('                              in setup mode and with ubuntu profile).')
    print('-f, --firewall=<ip>         : Firewall ip address (default *.*.*.254).')
    print('-g, --gateway=<ip>          : Gateway ip address (default is firewall ip).')
    print('-d, --domain=<domainname>   : Domainname (default linuxmuster.lan).')
    print('-r, --serverip=<serverip>   : Ip address of the server (unattended mode only).')
    print('-w, --swapsize=<size>       : Swapfile size in GB (default "2").')
    print('-a, --rootpw=<password>     : Set root password (only with -s).')
    print('-b, --reboot                : Reboots finally (only in unattended mode).')
    print('-u, --unattended            : Unattended mode, do not ask, use defaults.')
    print('-e, --default               : Sets default (10.0.0.0/16) network addresses,')
    print('                              triggers setup and unattended modes, needs profile')
    print('                              (uses saved profile from initial run).')
    print('-j, --no-keys               : Do not create ssh keys')
    print('                              (only in setup mode and together with -x).')
    print('-k, --no-pw                 : Do not set admin password')
    print('                              (only in setup mode and together with -x).')
    print('-h, --help                  : Print this help.')

    sys.exit(rc)


def parse_args():
    parser = argparse.ArgumentParser(description='Prepare a linuxmuster.net appliance installation.')
    parser.add_argument('-x', '--force', action='store_true', help='Force run on an already configured system.')
    parser.add_argument('-i', '--initial', action='store_true', help='Prepare the appliance initially for rollout.')
    parser.add_argument('-s', '--setup', action='store_true', help='Further appliance setup (network, swapsize).')
    parser.add_argument('-t', '--hostname', help='Hostname to apply (optional, works only with server profile).')
    parser.add_argument('-n', '--ipnet', help='Ip address and bitmask assigned to the host')
    parser.add_argument('-p', '--profile', choices=profile_list, help='Host profile to apply, mandatory. Expected values are "server" or "ubuntu".')
    parser.add_argument('-c', '--createcert', action='store_true', help='Create self signed server cert (to be used only in setup mode and with ubuntu profile).')
    parser.add_argument('-f', '--firewall', help='Firewall ip address (default *.*.*.254).')
    parser.add_argument('-g', '--gateway', help='Gateway ip address (default is firewall ip).')
    parser.add_argument('-d', '--domain', help='Domainname (default linuxmuster.lan).')
    parser.add_argument('-r', '--serverip', help='Ip address of the server (unattended mode only).')
    parser.add_argument('-w', '--swapsize', help='Swapfile size in GB (default "2").')
    parser.add_argument('-a', '--rootpw', help='Set root password (only with -s).')
    parser.add_argument('-b', '--reboot', action='store_true', help='Reboots finally (only in unattended mode).')
    parser.add_argument('-u', '--unattended', action='store_true', help='Unattended mode, do not ask, use defaults.')
    parser.add_argument('-e', '--default', action='store_true', help='Sets default (10.0.0.0/16) network addresses, triggers setup and unattended modes.')
    parser.add_argument('-j', '--no-keys', dest='no_keys', action='store_true', help='Do not create ssh keys (only in setup mode and together with -x).')
    parser.add_argument('-k', '--no-pw', dest='no_pw', action='store_true', help='Do not set admin password (only in setup mode and together with -x).')
    args = parser.parse_args()

    config = PrepareConfig(
        profile=args.profile,
        hostname=args.hostname,
        ipnet=args.ipnet,
        firewallip=args.firewall,
        gateway=args.gateway,
        domainname=args.domain if args.domain is not None else domainname,
        swapsize=args.swapsize if args.swapsize is not None else swapsize,
        rootpw=args.rootpw if args.rootpw is not None else rootpw_default,
        serverip=args.serverip if args.serverip is not None else serverip,
        initial=args.initial,
        setup=args.setup,
        unattended=args.unattended,
        force=args.force,
        createcert=args.createcert,
        nokeys=args.no_keys,
        nopw=args.no_pw,
        reboot=args.reboot,
    )

    if args.default:
        config.setup_mode = 'default'
        config.unattended = True
        config.setup = True

    return config


def apply_config(config):
    global profile, hostname, firewallip, gateway, ipnet, serverip
    global swapsize, rootpw, initial, setup, unattended, force, createcert, nokeys, nopw
    global reboot, setup_mode, domainname

    if config.profile is not None:
        profile = config.profile
    if config.hostname is not None:
        hostname = config.hostname
    if config.ipnet is not None:
        ipnet = config.ipnet
    if config.firewallip is not None:
        firewallip = config.firewallip
    if config.gateway is not None:
        gateway = config.gateway
    domainname = config.domainname
    swapsize = config.swapsize
    rootpw = config.rootpw
    serverip = config.serverip
    initial = config.initial
    setup = config.setup
    unattended = config.unattended
    force = config.force
    createcert = config.createcert
    nokeys = config.nokeys
    nopw = config.nopw
    reboot = config.reboot
    setup_mode = config.setup_mode


# test internet connection by deb.linuxmuster.net
def internet(host="deb.linuxmuster.net", port=443, timeout=3):
    """
    Host: deb.linuxmuster.net
    OpenPort: 443/tcp
    Service: https (http protocol over TLS/SSL)
    """
    try:
        socket.setdefaulttimeout(timeout)
        socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
        return True
    except Exception as ex:
        #print ex.message
        return False


# print without linefeed
def printr(msg):
    print(msg, end='', flush=True)


# return datetime string
def dtStr():
    return "{:%Y%m%d%H%M%S}".format(datetime.datetime.now())


# return content of text file
def readTextfile(tfile):
    if not os.path.isfile(tfile):
        return False, None
    try:
        infile = open(tfile, 'r')
        content = infile.read()
        infile.close()
        return True, content
    except:
        print('Cannot read ' + tfile + '!')
        return False, None


# write textfile
def writeTextfile(tfile, content, flag):
    try:
        outfile = open(tfile, flag)
        outfile.write(content)
        outfile.close()
        return True
    except:
        print('Failed to write ' + tfile + '!')
        return False


# replace string in file
def replaceInFile(tfile, search, replace):
    rc = False
    try:
        bakfile = tfile + '.bak'
        copyfile(tfile, bakfile)
        rc, content = readTextfile(tfile)
        rc = writeTextfile(tfile, content.replace(search, replace), 'w')
    except:
        print('Failed to write ' + tfile + '!')
        if os.path.isfile(bakfile):
            copyfile(bakfile, tfile)
    if os.path.isfile(bakfile):
        os.unlink(bakfile)
    return rc


# return detected network interfaces
def detectedInterfaces():
    iface_list = netifaces.interfaces()
    iface_list.remove('lo')
    iface_count = len(iface_list)
    if iface_count == 1:
        iface_default = iface_list[0]
    else:
        iface_default = None
    return iface_list, iface_default


# returns default network interface
def getDefaultIface():
    # first try to get a single interface
    iface_list, iface_default = detectedInterfaces()
    if iface_default is not None:
        return iface_list, iface_default
    # second if more than one get it by default route
    route = "/proc/net/route"
    with open(route) as f:
        for line in f.readlines():
            try:
                iface, dest, _, flags, _, _, _, _, _, _, _, =  line.strip().split()
                if dest != '00000000' or not int(flags, 16) & 2:
                    continue
                return iface_list, iface
            except:
                continue
    return iface_list, iface_default


# returns entered interface name
def inputIface(iface, iface_default, iface_list):
    while True:
        iface = input('Enter network interface to use ' + str(iface_list) + ': ')
        iface = iface or iface_default
        if iface in iface_list:
            return iface
        print("Invalid entry!")


# returns entered network address
def inputIpnet(ipnet):
    ipnet_saved = ipnet
    ipnet = input('Enter host ip address with net or bitmask [' + ipnet + ']: ')
    return ipnet or ipnet_saved


# returns entered firewall ip
def inputFirewallip(firewallip):
    firewallip_saved = firewallip
    firewallip = input('Enter firewall ip address [' + firewallip + ']: ')
    return firewallip or firewallip_saved


# returns entered gateway ip
def inputGateway(gateway):
    gateway_saved = gateway
    gateway = input('Enter gateway ip address [' + gateway + ']: ')
    return gateway or gateway_saved


# returns entered hostname
def inputHostname(hostname):
    hostname_saved = hostname
    hostname = input('Enter hostname [' + str(hostname) + ']: ')
    return hostname or hostname_saved


# returns entered domainname
def inputDomainname(domainname):
    domainname_saved = domainname
    domainname = input('Enter domainname [' + str(domainname) + ']: ')
    return domainname or domainname_saved


# returns entered swapsize
def inputSwapsize(swapsize):
    swapsize_saved = swapsize
    swapsize = input('Enter swap size in GiB [' + str(swapsize) + ']: ')
    return swapsize or swapsize_saved


# test valid hostname
def isValidHostname(hostname):
    try:
        if (len(hostname) > 63 or hostname[0] == '-' or hostname[-1] == '-'):
            return False
        allowed = re.compile(r'[a-z0-9\-]*$', re.IGNORECASE)
        if allowed.match(hostname):
            return True
        else:
            return False
    except:
        return False


# test valid domainname
def isValidDomainname(domainname):
    try:
        for label in domainname.split('.'):
            if not isValidHostname(label):
                return False
        return True
    except:
        return False


# test if ip matches network
def ipMatchNet(ip, network, bitmask):
    if not isValidHostIpv4(ip):
        return False
    try:
        ipnet = ip + '/' + bitmask
        n = IP(ipnet, make_net=True)
        network_ip = IP(n).strNormal(0)
        if network == network_ip:
            return True
        else:
            return False
    except:
        return False


# test valid ipv4 address
def isValidHostIpv4(ip):
    try:
        ipv4 = IP(ip)
        if not ipv4.version() == 4:
            return False
        ipv4str = IP(ipv4).strNormal(0)
        if (int(ipv4str.split('.')[0]) == 0 or int(ipv4str.split('.')[3]) == 0):
            return False
        for i in ipv4str.split('.'):
            if int(i) > 254:
                return False
        return True
    except:
        return False


# remove unnecessary stuff
def do_cleaning():
    print('## Remove unnecessary stuff')
    run_shell_command('DEBIAN_FRONTEND=noninteractive apt-get -y purge ' + purgepkgs, shell=True)


# check and configure locale
def do_locale():
    print('## Check and configure locale')
    proc = run_shell_command('locale', shell=True, capture_output=True)
    locale_output = proc.stdout if hasattr(proc, 'stdout') and proc.stdout is not None else ''
    lang_match = re.search(r'^LANG=(.+)$', locale_output, re.MULTILINE)

    if lang_match and lang_match.group(1).strip():
        print('# Locale already configured: LANG=' + lang_match.group(1).strip())
        return

    print('# Locale not configured, running dpkg-reconfigure locales')
    rc = run_shell_command('DEBIAN_FRONTEND=readline dpkg-reconfigure locales', shell=True)
    if rc != 0:
        print('# dpkg-reconfigure locale failed, trying dpkg-reconfigure locales')
        rc = run_shell_command('DEBIAN_FRONTEND=readline dpkg-reconfigure locales', shell=True)
    if rc != 0:
        print('Locale configuration failed!')
        sys.exit(rc)


# check and configure timezone
def do_timezone():
    print('## Check and configure timezone')
    proc = run_shell_command('timedatectl', shell=True, capture_output=True)
    timedatectl_output = proc.stdout if hasattr(proc, 'stdout') and proc.stdout is not None else ''
    timezone_match = re.search(r'Time zone:\s+(.+?)\s', timedatectl_output)

    if timezone_match and timezone_match.group(1).strip():
        print('# Timezone already configured: ' + timezone_match.group(1).strip())
        return

    print('# Timezone not configured, running dpkg-reconfigure tzdata')
    rc = run_shell_command('DEBIAN_FRONTEND=readline dpkg-reconfigure tzdata', shell=True)
    if rc != 0:
        print('Timezone configuration failed!')
        sys.exit(rc)


# add pre-mount script for quota and rebuild initramfs
def do_dracut():
    print('# Add mount options for quota.')
    run_shell_command('/usr/bin/dracut --force --add linuxmuster', shell=True)


# swap file
def do_swap(swapsize):
    print('## Swapfile')
    gbsize = swapsize + 'G'
    if not os.path.isfile(swapfile):
        print('No swapfile found, skipping this step!')
        return
    run_shell_command('swapoff ' + swapfile, shell=True)
    run_shell_command('rm ' + swapfile, shell=True)
    if unattended == True:
        try:
            run_shell_command('fallocate -l ' + gbsize + ' ' + swapfile, shell=True)
        except:
            print('Cannot create ' + swapfile + '!')
            sys.exit(1)
    else:
        while True:
            defaultsize = swapsize
            swapsize = input('Enter the size of the swapfile in GB [' + defaultsize + ']: ')
            swapsize = swapsize or defaultsize
            gbsize = swapsize + 'G'
            rc = run_shell_command('fallocate -l ' + gbsize + ' ' + swapfile, shell=True)
            if rc == 0:
                break
            else:
                print("Invalid entry!")
    run_shell_command('chmod 600 ' + swapfile, shell=True)
    run_shell_command('mkswap ' + swapfile, shell=True)
    run_shell_command('swapon ' + swapfile, shell=True)
    run_shell_command('swapon --show', shell=True)


# root password
def do_password(rootpw):
    print('## Passwords')
    printr('# root ... ')
    rc = run_shell_command('echo "root:' + rootpw + '" | chpasswd', shell=True)
    if rc == 0:
        print('OK!')
    else:
        print('Failed!')
    for item in user_list:
        try:
            pwd.getpwnam(item)
            printr('# ' + item + ' ... ')
            rc = run_shell_command('echo "' + item + ':' + rootpw + '" | chpasswd', shell=True)
            print('OK!')
        except KeyError:
            continue


# create ssh hostkeys
def do_keys():
    print('## SSH host keys')
    hostkey_prefix = '/etc/ssh/ssh_host_'
    crypto_list = ['dsa', 'ecdsa', 'ed25519', 'rsa']
    run_shell_command('rm -f /etc/ssh/*key*', shell=True)
    for a in crypto_list:
        printr(' * ' + a + ' host key:')
        try:
            run_shell_command('ssh-keygen -t ' + a + ' -f ' + hostkey_prefix + a + '_key -N ""', shell=True)
            print(' Success!')
        except:
            print(' Failed!')
            sys.exit(1)


# create ssl certs (ubuntu only)
def do_sslcert(profile, domainname):
    print('## SSL certificate')
    ssldir = '/etc/linuxmuster/ssl'
    csrfile = ssldir + '/' + profile + '.csr'
    keyfile = ssldir + '/' + profile + '.key.pem'
    certfile = ssldir + '/' + profile + '.cert.pem'
    days = '3653'
    run_shell_command('mkdir -p ' + ssldir, shell=True)
    run_shell_command('openssl genrsa -out ' + keyfile + ' 2048', shell=True)
    run_shell_command('chmod 640 ' + keyfile, shell=True)
    run_shell_command('echo -e "\n\n\n' + domainname + '\n' + profile + '\n' + profile
        + '\n\n\n\n" | openssl req -new -key ' + keyfile + ' -out ' + csrfile, shell=True)
    run_shell_command('openssl x509 -req -days ' + days + ' -in '
        + csrfile + ' -signkey ' + keyfile + ' -out ' + certfile, shell=True)


# updates if internet connection is available
def do_updates():
    if not internet():
        print('# No internet connection!')
        sys.exit(1)
    print('## Installing updates')
    run_shell_command('DEBIAN_FRONTEND=noninteractive apt-get update', shell=True)
    res = run_shell_command('DEBIAN_FRONTEND=noninteractive apt-get -q=2 dist-upgrade', shell=True)
    run_shell_command('apt-get clean && apt-get -q=2 autoremove', shell=True)
    if res != 0:
        print('Updates failed!')
        sys.exit(res)


# install a space separated list of package names
def do_install(pkgs):
    if pkgs is None:
        return
    print('## Installing software')
    run_shell_command('DEBIAN_FRONTEND=noninteractive apt-get -q=2 install ' + pkgs, shell=True)
    run_shell_command('apt-get clean', shell=True)
    # check if essential pkgs are installed correctly
    for pkg in pkgs.split():
        print('Checking ' + pkg + ' ... ', end = '')
        res = run_shell_command('dpkg -l | grep ^"ii  ' + pkg + '"', shell=True)
        if res != 0:
            print('Failed!')
            sys.exit(1)
        else:
            print('OK.')


# merge inifiles
def mergeInis():
    print('## Merging inifiles:')
    setup = configparser.RawConfigParser(delimiters=('='))
    for item in [setupini, prepini]:
        # skip non existant file
        if not os.path.isfile(item):
            print('# ' + item + ' not found!')
            return False
        # reading setup values
        print('# Reading ' + item)
        setup.read(item)
    # writing setup.ini
    print('# Writing ' + setupini)
    try:
        with open(setupini, 'w') as outfile:
            setup.write(outfile)
    except:
        print('# Writing failed!')
        return False
    # remove prepare.ini
    os.unlink(prepini)


# print setup values
def print_values(profile, hostname, domainname, hostip, netmask, firewallip, iface, swapsize):
    print('\n## The system has been prepared with the following values:')
    print('# Profile   : ' + profile)
    print('# Hostname  : ' + hostname)
    print('# Domain    : ' + domainname)
    print('# IP        : ' + hostip)
    print('# Netmask   : ' + netmask)
    print('# Firewall  : ' + firewallip)
    print('# Gateway   : ' + gateway)
    print('# Interface : ' + iface)
    print('# Swapsize  : ' + swapsize + 'G')

## functions end


run_shell_command('clear', shell=True)
print('### lmn-prepare')

# get cli args
config = parse_args()
apply_config(config)

# read saved profile from previous run
inifile = None
if os.path.isfile(prepini):
    inifile = prepini
elif os.path.isfile(setupini):
    inifile = setupini
if inifile is not None:
    print('## Reading profile from ' + inifile + ':')
    prep = configparser.RawConfigParser(delimiters=('='))
    prep.read(inifile)
    try:
        profile_ini = prep.get('setup', 'profile')
        if profile_ini is not None:
            profile = profile_ini
            print('# saved profile: ' + profile_ini)
    except:
        pass

# exit if system is already set up
if force:
    print("## Force is given, skipping test for configured system.")
else:
    if os.path.isfile(setupini):
        print("## Don't do this on an already configured system!")
        sys.exit(1)

# check mandatory values
if setup_mode is not None:
    if profile is None and profile_ini is not None:
        profile = profile_ini
if not profile in profile_list or profile is None:
    print('Invalid profile!')
    usage(1)
if profile != 'server':
    hostname = profile
else:
    if hostname is None:
        hostname = 'server'
if profile is None:
    usage(1)
if profile == 'server':
    ipnr = '1'
else:
    ipnr = '21'
if setup is False and initial is False:
    print('You have to provide either -i or -s!')
    usage(1)
if setup == True and initial == True:
    print("-i and -s don't work together!")
    usage(1)
if setup is False and profile != 'ubuntu' and createcert == True:
    print("-c can only be used together with -s and ubuntu profile!")
    usage(1)
if setup is False and nokeys == True:
    print("-j can only be used in setup mode!")
    usage(1)
if force is False and nokeys == True:
    print("-j can only be used together with -x!")
    usage(1)
if setup is False and nopw == True:
    print("-k can only be used in setup mode!")
    usage(1)
if force is False and nopw == True:
    print("-k can only be used together with -x!")
    usage(1)
if not unattended:
    # do not set in interactive mode
    reboot = False


# main
print('## Profile: ' + profile)


# get & check network values -- begin
print('## Network')

# get network interface
iface_list, iface_default = getDefaultIface()
if len(iface_list) == 0:
    print('# No network interfaces found!')
    sys.exit(1)
# input interface if not unattended
iface = inputIface(iface, iface_default, iface_list) if not unattended else iface_default

# set default ip address & netmask
if setup_mode == 'default' or ipnet is None:
    ipnet = ipnet_default + ipnr + '/' + bitmask_default

# host ip/net
ipnet_saved = ipnet
while True:
    ipnet = inputIpnet(ipnet_saved) if not unattended else ipnet_saved
    try:
        n = IP(ipnet, make_net=True)
        hostip = ipnet.split('/')[0]
        network = IP(n).strNormal(0)
        bitmask = IP(n).strNormal(1).split('/')[1]
        netmask = IP(n).strNormal(2).split('/')[1]
        broadcast = IP(n).strNormal(3).split('-')[1]
        o1, o2, o3, o4 = hostip.split('.')
        break
    except:
        print(f'# {ipnet} is not valid!')
        sys.exit(1) if unattended else print('Try again.')

# firewall ip
if firewallip is None:
    firewallip = o1 + '.' + o2 + '.' + o3 + '.254'
firewallip_saved = firewallip
while True:
    firewallip = inputFirewallip(firewallip_saved) if not unattended else firewallip_saved
    if ipMatchNet(firewallip, network, bitmask):
        break
    else:
        print(f'# {firewallip} is not valid!')
        sys.exit(1) if unattended else print('Try again.')

# gateway ip
if gateway is None:
    gateway = firewallip
gateway_saved = gateway
while True:
    gateway = inputGateway(gateway_saved) if not unattended else gateway_saved
    if ipMatchNet(gateway, network, bitmask):
        break
    else:
        print(f'# {gateway} is not valid!')
        sys.exit(1) if unattended else print('Try again.')
 
# hostname
hostname_saved = hostname
while True:
    hostname = inputHostname(hostname_saved) if not unattended else hostname_saved
    if isValidHostname(hostname):
        break
    else:
        print(f'# {hostname} is not valid!')
        sys.exit(1) if unattended else print('Try again.')

# domainname
domainname_saved = domainname
while True:
    domainname = inputDomainname(domainname_saved) if not unattended else domainname_saved
    if isValidDomainname(domainname):
        break
    else:
        print(f'# {domainname} is not valid!')
        sys.exit(1) if unattended else print('Try again.')
# get & check network values -- end




# get & check swapsize
print('## Swap')
swapsize_saved = swapsize
while True:
    swapsize = inputSwapsize(swapsize_saved) if not unattended else swapsize_saved
    if str.isdigit(swapsize):
        break
    else:
        print(f'# Swapsize is not an integer!')
        sys.exit(1) if unattended else print('Try again.')


# initial prepare
if initial:
    serverip = ''
    rootpw = rootpw_default
    do_updates()
    do_cleaning()
    # check lvm data
    if profile == 'server':
        do_locale()
        do_timezone()
        do_dracut()
        do_install(srvpkgs)
    # activate serial console
    run_shell_command('systemctl enable serial-getty@ttyS0.service', shell=True)
    run_shell_command('systemctl start serial-getty@ttyS0.service', shell=True)
    do_password(rootpw)
    dnssearch = ''
# setup mode
elif setup:
    do_swap(swapsize)
    # write hostname before keys and certificates were created
    writeTextfile('/etc/hostname', hostname + '.' + domainname, 'w')
    if not nokeys:
        do_keys()
    if profile == 'ubuntu' and createcert:
        do_sslcert(profile, domainname)
    if not nopw:
        do_password(rootpw)
    dnssearch = 'search: [' + domainname + ']'


# write configs, common and issue specific
print('## Writing configuration')
run_shell_command('mkdir -p ' + libdir, shell=True)
# delete cloud-init netcfg if present (we provide our own)
if os.path.isdir('/etc/netplan'):
    run_shell_command('rm -f /etc/netplan/*.yaml', shell=True)
for item in os.listdir(templates):
    rc, content = readTextfile(templates + '/' + item)
    # extract oufile path from first line
    firstline = re.findall(r'# .*\n', content)[0]
    outfile = firstline.partition(' ')[2].replace('\n', '')
    # replace placeholders
    content = content.replace('@@iface@@', iface)
    content = content.replace('@@hostip@@', hostip)
    content = content.replace('@@hostname@@', hostname)
    content = content.replace('@@bitmask@@', bitmask)
    content = content.replace('@@netmask@@', netmask)
    content = content.replace('@@network@@', network)
    content = content.replace('@@broadcast@@', broadcast)
    content = content.replace('@@profile@@', profile)
    content = content.replace('@@firewallip@@', firewallip)
    content = content.replace('@@gateway@@', gateway)
    content = content.replace('@@domainname@@', domainname)
    content = content.replace('@@dnssearch@@', dnssearch)
    content = content.replace('@@swapsize@@', swapsize)
    #content = content.replace('@@resolvconf@@', resolvconf)
    if outfile == prepini and profile == 'server':
        content = content.replace('@@serverip@@', hostip)
    else:
        content = content.replace('@@serverip@@', serverip)
    # repair missing serverip in netcfg.yaml
    content = content.replace('[,', '[')
    # add date string
    content = '# modified by lmn71-prepare at ' + dtStr() + '\n' + content
    # write outfile
    writeTextfile(outfile, content, 'w')

# merge inifiles if setup.ini is present
if os.path.isfile(setupini):
    mergeInis()

# save hostname
outfile = '/etc/hostname'
content = hostname + '.' + domainname
writeTextfile(outfile, content, 'w')

# print values
print_values(profile, hostname, domainname, hostip, netmask, firewallip, iface, swapsize)

print('\n### Finished - a reboot is necessary!')

if not updates:
    print("\n### Don't forget to dist-upgrade the system before setup!")

if unattended and reboot:
    print('\n### Reboot requested ...')
    run_shell_command('/sbin/reboot', shell=True)
