BACKD00R 1337
Server IP : 164.52.202.56  /  Your IP : 216.73.216.208
Web Server : Apache
System : Linux e2e-70-56.ssdcloudindia.net 4.18.0-553.27.1.el8_10.x86_64 #1 SMP Tue Nov 5 04:50:16 EST 2024 x86_64
User : rubyaromatics ( 1052)
PHP Version : 7.2.34
Directory (0555) :  /sbin/../sbin/

[  Home  ][  Terminal  ][  Upload File  ]

Current File : //sbin/../sbin/onesysprep
#!/bin/sh

# ---------------------------------------------------------------------------- #
# Copyright 2020-2021, OpenNebula Project, OpenNebula Systems                  #
#                                                                              #
# Licensed under the Apache License, Version 2.0 (the "License"); you may      #
# not use this file except in compliance with the License. You may obtain      #
# a copy of the License at                                                     #
#                                                                              #
# http://www.apache.org/licenses/LICENSE-2.0                                   #
#                                                                              #
# Unless required by applicable law or agreed to in writing, software          #
# distributed under the License is distributed on an "AS IS" BASIS,            #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.     #
# See the License for the specific language governing permissions and          #
# limitations under the License.                                               #
# ---------------------------------------------------------------------------- #

# This script tries to reimplement libguestfs.org's sysprep tool while trying
# to support as much sysprep's operations (below) as possible. The intention
# behind this tool is to provide the almost same functionality but from within
# the running VM (as opposed to the original libguest's sysprep).
#
# On top of the original operations there are extra new operations - prefixed
# with 'one-' to avoid a naming conflict. These are specific to this tool and
# extend the original functionality which was too much redhat-centric.

set -e

################################################################################
# globals
#

CMD=$(basename "$0")
CMDLINE="${0} ${*}"

# here are declared all sysprep operations - keep the following format:
#   <operation>:<default>:<comment>
# for each operation there should be corresponding function:
#   op_<operation-with_underscores>
ALL_SYSPREP_OPERATIONS='
abrt-data               :1: Remove the crash data generated by ABRT
backup-files            :1: Remove editor backup files from the guest
bash-history            :1: Remove the bash history in the guest
blkid-tab               :1: Remove blkid tab in the guest
ca-certificates         :0: Remove CA certificates in the guest
crash-data              :1: Remove the crash data generated by kexec-tools
cron-spool              :1: Remove user at-jobs and cron-jobs
customize               :1: (NOT IMPLEMENTED)
dhcp-client-state       :1: Remove DHCP client leases
dhcp-server-state       :1: Remove DHCP server leases
dovecot-data            :1: Remove Dovecot (mail server) data
firewall-rules          :0: Remove the firewall rules
flag-reconfiguration    :0: Flag the system for reconfiguration
fs-uuids                :0: (NOT IMPLEMENTED)
kerberos-data           :0: Remove Kerberos data in the guest
logfiles                :1: Remove many log files from the guest
lvm-uuids               :1: (NOT IMPLEMENTED)
machine-id              :1: Remove the local machine ID
mail-spool              :1: Remove email from the local mail spool directory
net-hostname            :1: Remove HOSTNAME and DHCP_HOSTNAME
net-hwaddr              :1: Remove HWADDR (hard-coded MAC address)
pacct-log               :1: Remove the process accounting log files
package-manager-cache   :1: Remove package manager cache
pam-data                :1: Remove the PAM data in the guest
passwd-backups          :1: Remove /etc/passwd- and similar backup files
puppet-data-log         :1: Remove the data and log files of puppet
rh-subscription-manager :1: Remove the RH subscription manager files
rhn-systemid            :1: Remove the RHN system ID
rpm-db                  :1: Remove host-specific RPM database files
samba-db-log            :1: Remove the database and log files of Samba
script                  :1: (NOT IMPLEMENTED)
smolt-uuid              :1: Remove the Smolt hardware UUID
ssh-hostkeys            :1: Remove the SSH host keys in the guest
ssh-userdir             :1: Remove ".ssh" directories in the guest
sssd-db-log             :1: Remove the database and log files of sssd
tmp-files               :1: Remove temporary files
udev-persistent-net     :1: Remove udev persistent net rules
user-account            :0: Remove the user accounts in the guest
utmp                    :1: Remove the utmp file
yum-uuid                :1: Remove the yum UUID
one-cleanup             :1: Remove OpenNebula-owned working directories
one-shell-history       :1: Remove the .history file
one-hostname            :1: Remove hostname and fix hosts file
one-resolvconf          :1: Remove nameservers
one-network             :1: Nuke all the networking configuration
one-zerofill            :0: Fill the free space with zeroes and discard it
one-trim                :1: Trim the discarded/unused space
'

################################################################################
# functions
#

usage()
(
    cat <<EOF
NAME
    ${CMD} - OpenNebula System Preparation Tool

DESCRIPTION
    Tool for cleaning up the system from sensitive or undesirable information
    and preparing it as a base for cloning or instantiation of VMs.

    It tries to meet the functionality of the libguestfs sysprep tool with the
    exception of being able to run from within already running virtual machine.
    While the libguestfs sysprep operates on the poweroffed system and modifies
    its disk image from outside. The libguestfs approach is safer but it also
    means that not all images and their filesystems can be supported.

    The idea is to prepare image for further usage - for example like this:
        1. instantiate the VM on top of a persistent image
        2. create necessary changes (updates, edits, add data, files, etc.)
        3. optionally switch to the single user mode (if not updating packages)
        4. run '${CMD}'
        5. poweroff (manually or via ${CMD})
        6. and lastly switch the image to non-persistent (!)

USAGE
    ${CMD} --help
        This help

    ${CMD} --list-operations
        It will list all supported operations and mark the default one with the
        asterisk '*'.

    ${CMD} [--operations <operation-list>]... [--yes] [--strict] [--update]
           [--selinux-relabel] [--remove-user-accounts <user-list>]
           [--keep-user-accounts <user-list>] [--root-password <selector>]
           [--password <user:selector>] [--verbose] [--poweroff]

    '--operations'

        If no '--operations' nor one option 'yes' is used then ${CMD} will
        first ask you interactively if you really want to run this command and
        if so then it will proceed with the execution of the default set of
        operations.

        With provided '--operations' or one option 'yes' it will execute
        (without prompting you) the desired operations or default ones if no
        other is specified.

        '--operations' can be used more than once - the lists are then spliced
        together.

        The argument is a comma separated list of operations where amongst the
        actual names of the operations are also recognized special:
            'default' and 'all'

        Their meaning is obvious. For the list of all supported operations
        use the '--list-operations'.

        Operation can also be prefixed with the '-' which will mean that this
        particular operation will not be executed even if it would otherwise.

    '--yes'

        Never ask for permission - run non-interactively - otherwise in some
        circumstances (for your own safety) program will refuse to continue
        until you type 'yes'.

    '--strict'

        Abort the run of this program if system is not recognized.

    '--update'

        It will update installed packages.

    '--selinux-relabel'

        It will relabel all files if the system supports SElinux.

    '--remove-user-accounts'

        It will add users to the list of accounts to be deleted. When a user is
        listed in both '--remove-user-accounts' and '--keep-user--accounts'
        then the latter has a precedence and such user is preserved.

    '--keep-user-accounts'

        When there is a request for user account deletion either via previous
        parameter or by 'user-account' operation - users specified here will be
        whitelisted and preserved in all cases.

    '--root-password'

        This parameter manages root account - selector here is one of:
            file:FILENAME
            password:PASSWORD
            random
            disabled
            locked
            locked:file:FILENAME
            locked:password:PASSWORD
            locked:random
            locked:disabled

    '--password'

        Same as with the root password but selector is on-top prefixed with the
        username, e.g.: username:locked:disabled

    '--verbose'

        Print executed commands and show their output.

    '--poweroff'

        At the end of the run of ${CMD} - poweroff the system.

        Although no such parameter is in virt-sysprep (because it operates on
        the image of an already poweroffed system) - it makes sense here.

EXAMPLES
    ${CMD}
        Run all default operations but only if explicitly replied 'YES'

    ${CMD} --operations default,-cron-spool
        Run all default operations EXCEPT the cron-spool

    ${CMD} --operations machine-id,ca-certificates
        Run only these two operations

    ${CMD} --yes --poweroff --root-password locked:disabled
        Run all default operations, delete and lock root password and
        poweroff the system immediately after - no questions asked

    ${CMD} --yes --strict --operations all,-one-zerofill
        Run all the available operations *EXCEPT* the 'one-zerofill' and
        without asking but abort promptly if system is not recognized.

        BEWARE: 'one-zerofill' operation will fill up the free space of the VM
        with zeroes to claim the unused space. By doing this the disk can be
        effectively sparsified by the trim command in the next step.  The issue
        is that zerofill must basically claim the size of the whole disk - if
        the definition of your disk image does not have discard='unmap' (on
        QEMU/KVM for example) and the host system does not support it then you
        will end up with *MAXIMIZED* disk image size instead of the shrinked
        size...

EOF
)

print_help()
(
    printf "${SETCOLOR_ON_COMMAND}[!] try help: %s${SETCOLOR_OFF}\n" \
        "${CMD} --help" >&2
)

msg()
(
    printf "${SETCOLOR_ON_HIGHLIGHT2}[%s]${SETCOLOR_OFF}: ${*}\n" \
        "$CMD"
)

err()
(
    {
        printf "${SETCOLOR_ON_HIGHLIGHT2}[%s]${SETCOLOR_OFF}" \
            "$CMD"
        printf " ${SETCOLOR_ON_FAILURE}%s${SETCOLOR_OFF}\n" \
            "[!] ERROR: ${*}"
    } >&2
)

warn()
(
    {
        printf "${SETCOLOR_ON_HIGHLIGHT2}[%s]${SETCOLOR_OFF}" \
            "$CMD"
        printf " ${SETCOLOR_ON_FAILURE}%s${SETCOLOR_OFF}\n" \
            "[!] WARNING: ${*}"
    } >&2
)

# arg(s): <command> <arg>...
run_cmd()
{
    if is_one_option 'verbose' ; then
        # verbose command execution
        printf "${SETCOLOR_ON_HIGHLIGHT2}[%s]${SETCOLOR_OFF}:" \
            "$CMD"
        printf " ${SETCOLOR_ON_COMMAND}%s${SETCOLOR_OFF}\n" \
            "${*}"
    fi

    if ! "$@" && [ "$IGNORE_ERROR" != 'yes' ] ; then
        err "Command failed - ABORT"
        exit 1
    fi
}

# trivial test of awk command capabilities and compliance - it will output
# either YES or NO
is_awk_correct()
(
    _awk_result=$(echo 'abcABC   xyz123.' | awk '
        {
            sub(/[[:upper:]]+[[:space:]]+[[:alnum:]]+/, "");
            print;
        }')

    if [ "${_awk_result}" = 'abc.' ] ; then
        echo 'YES'
    else
        echo 'NO'
    fi
)

# arg: [normal|summary]
# if used with argument 'summary' then it will print out summary of supported
# operations - otherwise it will output parseable: <op>:<default>
parse_sysprep_operations()
(
    echo "$ALL_SYSPREP_OPERATIONS" | awk -v output="${1:-normal}" '
    BEGIN {
        FS = ":";
        count=0;
    }
    {
        if ($0 == "")
            next;

        op = $1;
        sub(/^[ ]*/, "", op);
        sub(/[ ]*$/, "", op);

        default_op = $2;
        sub(/^[ ]*/, "", default_op);
        sub(/[ ]*$/, "", default_op);

        comment = $3;
        sub(/^[ ]*/, "", comment);
        sub(/[ ]*$/, "", comment);

        count++;
        ops[count] = op;
        ops_comment[op] = comment;
        ops_default[op] = default_op;
    }
    END {
        if (output == "normal"){
            for (i = 1; i <= count; i++) {
                op = ops[i];
                printf("%s:%s\n", op, ops_default[op]);
            }
        } else if (output == "summary") {
            max_length = 0;
            for (i = 1; i <= count; i++) {
                if (length(ops[i]) > max_length)
                    max_length = length(ops[i]);
            }

            for (i = 1; i <= count; i++) {
                op = ops[i];
                if (ops_default[op] == "1")
                    asterisk = "*";
                else
                    asterisk = " ";
                # this sadly is not supported in busybox...
                # printf("%s %-*s %s\n", asterisk, max_length, op, ops_comment[op]);
                # busybox workaround
                extra_spaces_count = max_length - length(op);
                extra_spaces = "";
                for (s = 1; s <= extra_spaces_count; s++)
                    extra_spaces = extra_spaces " ";
                printf("%s %s%s %s\n", asterisk, op, extra_spaces, ops_comment[op]);
            }
        }
    }
    '
)

list_operations()
(
    printf "LIST OF ALL SUPPORTED OPERATIONS:\n\n"
    printf "(asterisk '*' designates a default operation)\n\n"

    parse_sysprep_operations summary
)

ask_yes()
(
    _reply=''

    while [ -z "$_reply" ] ; do
        printf "%s (${SETCOLOR_ON_COMMAND}%s${SETCOLOR_OFF})? " \
            "Do you really want to continue" \
            "'y/yes/Y/YES'"
        read -r _reply

        case "$_reply" in
            y|Y|yes|YES)
                return 0
                ;;
        esac
    done

    return 1
)

ask_to_run_sysprep()
{
    # shellcheck disable=SC2059
    printf "${SETCOLOR_ON_FAILURE}"
    printf "[!] BEWARE: This will erase some system data!\n"
    printf "[!] If you are not certain what this program does - try help:\n"
    printf "    %s --help\n\n" "${CMD}"
    # shellcheck disable=SC2059
    printf "${SETCOLOR_OFF}"

    if ! is_one_option 'yes' && ! ask_yes ; then
        msg "ABORTED"
        exit 0
    fi
}

ask_to_enter_single_user_mode()
{
    warn "You are about to enter the single user mode - this program will be terminated!"
    printf "[!] NOTE: You must invoke this program again once you reach the single user mode:\n"
    printf "    %s\n\n" "${CMDLINE}"

    if ! is_one_option 'yes' && ! ask_yes ; then
        msg "ABORTED"
        exit 0
    fi
}

ask_to_poweroff()
{
    warn "You are about to poweroff this system!"

    if ! is_one_option 'yes' && ! ask_yes ; then
        msg "ABORTED"
        exit 0
    fi
}

ask_to_proceed_on_unsupported_system()
{
    if ! is_one_option 'yes' && ! ask_yes ; then
        msg "ABORTED"
        exit 0
    fi

    warn "We will continue on the best-effort basis..."
}

# arg: all|default|<op>
# it will return (based on argument):
#   list of all operations
#   list of all default operations
#   operation if exists
#   empty string if operation does not exist
get_operations()
(
    case "$1" in
        all)
            parse_sysprep_operations normal | cut -d: -f1
            ;;
        default)
            parse_sysprep_operations normal | sed -n 's/^\(.*\):1$/\1/p'
            ;;
        *)
            parse_sysprep_operations normal | sed -n "s/^\(${1}\):[0-1]\$/\1/p"
            ;;
    esac
)

sanity_check()
{
    # test the sanity of an awk implementation
    _ONE_VALID_AWK=$(is_awk_correct)
    export _ONE_VALID_AWK

    if [ "${_ONE_VALID_AWK}" = 'NO' ] ; then
        warn "The currently used awk command does not support POSIX:"
        printf "\n%s\n\n" "$(awk -W version 2>&1 || true)"
        printf "[!] NOTE: Install an alternative (e.g. gawk) or some operations may be skipped!\n\n"
    fi
}

# this will try to guess the system it is running on and if it is supported by
# this script
syscheck()
{
    # detect the OS
    if [ -f /etc/os-release ] ; then
        ID=
        # shellcheck disable=SC1091
        . /etc/os-release
        _ONE_OS_ID=$(echo "$ID" | tr '[:upper:]' '[:lower:]')

    # check for legacy RHEL/CentOS 6
    elif [ -f /etc/centos-release ]; then
        _ONE_OS_ID='centos'
    elif [ -f /etc/redhat-release ]; then
        _ONE_OS_ID='rhel'

    else
        _ONE_OS_ID=$(uname | tr '[:upper:]' '[:lower:]')
    fi

    # return success if listed here:
    #
    # (I am breaking these into more cases just to avoid having long lines)
    case "$_ONE_OS_ID" in
        alpine|altlinux)
            return 0
            ;;
        debian|ubuntu|devuan)
            return 0
            ;;
        fedora|centos|rhel|almalinux|ol|rocky|amzn)
            return 0
            ;;
        opensuse*|sles|sled)
            return 0
            ;;
        freebsd)
            return 0
            ;;
    esac

    # [!] we failed the check - this system is not supported...

    # print the warning
    warn "This system ('${_ONE_OS_ID:-UNKNOWN}') is not supported!"

    # possibly abort
    if is_one_option 'strict' ; then
        err "System check failed while 'strict' option used - ABORT"
        exit 1
    else
        ask_to_proceed_on_unsupported_system
        return 0
    fi

    return 1
}

# it will either switch to the single user mode (if possible) or it will return
# immediately when it is already in one
enter_single_mode()
{
    # are we already switched?
    case "$_ONE_OS_ID" in
        alpine)
            _runlevel=$(rc-status -r)
            if [ "$_runlevel" = 'single' ] ; then
                return 0
            fi
            ;;
        debian|ubuntu|devuan|fedora|centos|rhel|almalinux|ol|rocky|altlinux|opensuse*)
            _runlevel=$(runlevel | cut -d" " -f2)
            case "$_runlevel" in
                1|S)
                    return 0
                    ;;
            esac
            ;;
        freebsd)
            warn "FreeBSD has no (clever) means of detecting the current runlevel - SKIP"
            printf "[!] NOTE: You can enter the single user mode prior to running this program:\n"
            printf "    shutdown now\n\n"

            if ! is_one_option 'yes' && ! ask_yes ; then
                msg "ABORTED"
                exit 0
            fi
            ;;
        *)
            warn "Unable to detect the runlevel on this system ('${_ONE_OS_ID:-UNKNOWN}')"
            printf "[!] NOTE: You can abort this script and switch manually if you wish...\n\n"

            if ! is_one_option 'yes' && ! ask_yes ; then
                msg "ABORTED"
                exit 0
            fi
            ;;
    esac

    # we are not in single user mode - so we will attempt to enter it...
    case "$_ONE_OS_ID" in
        alpine)
            ask_to_enter_single_user_mode
            msg "Entering single user mode..."
            rc single
            ;;
        debian|ubuntu|devuan|fedora|centos|rhel|almalinux|ol|rocky|altlinux|opensuse*)
            ask_to_enter_single_user_mode
            msg "Entering single user mode..."
            telinit 1
            ;;
        freebsd)
            :
            # TODO: for future reference:
            #shutdown now
            ## enable rw on rootfs again
            #/sbin/mount -urw /
            ;;
    esac
}

# arg: <passwd-selector-list>
# return: user:locked:disabled:file:password:random:value...
parse_password_arguments()
{
    _raw_password_list=$(echo "$1" | tr ',' ' ')
    _parsed_password_list=

    # selector should have format:
    # <user>:[locked:]disabled|file:<filename>|password:<passwd>|random
    for _selector in ${_raw_password_list} ; do
        _username=$(echo "$_selector" | cut -d":" -f1)
        _locked_or_atr=$(echo "$_selector" | cut -d":" -f2)
        _atr_or_value=$(echo "$_selector" | cut -d":" -f3)
        _value=$(echo "$_selector" | cut -d":" -f4)
        _error=$(echo "$_selector" | cut -d":" -f5)

        # simple check to catch extra fields
        if [ -n "$_error" ] ; then
            err "Wrong password selector: ${_selector} - ABORT"
            exit 1
        fi

        # selector attributes
        _locked=
        _disabled=
        _file=
        _password=
        _random=

        case "$_locked_or_atr" in
            locked)
                _locked=yes
                ;;
            disabled)
                _disabled=yes
                ;;
            file)
                _file=yes
                ;;
            password)
                _password=yes
                ;;
            random)
                _random=yes
                ;;
            *)
                err "Wrong password selector: ${_selector} - ABORT"
                exit 1
                ;;
        esac

        case "$_atr_or_value" in
            '')
                # is missing a value?
                if [ -n "${_file}${_password}" ] ; then
                    # selector is not complete
                    err "Wrong password selector: ${_selector} - ABORT"
                    exit 1
                fi
                ;;
            disabled|file|password|random)
                # it can be non-locked attribute or a value
                if [ -n "${_locked}" ] ; then
                    # locked was set - so this is attribute
                    eval "_${_atr_or_value}=yes"
                elif [ -z "${_value}${_disabled}${_random}" ] ; then
                    # it must be value
                    _value="$_atr_or_value"
                else
                    err "Wrong password selector: ${_selector} - ABORT"
                    exit 1
                fi
                ;;
            *)
                # it must be value or error
                if [ -z "${_locked}${_value}${_disabled}${_random}" ] ; then
                    _value="$_atr_or_value"
                else
                    err "Wrong password selector: ${_selector} - ABORT"
                    exit 1
                fi
                ;;
        esac

        # we should have all the pieces...

        _passwd_arg="${_username}:${_locked}:${_disabled}:${_file}:${_password}:${_random}:${_value}"
        _parsed_password_list="${_parsed_password_list} ${_passwd_arg}"
    done

    echo "$_parsed_password_list"
}

# arg: <length>
# shellcheck disable=SC2120
gen_password()
{
    pw_length="${1:-16}"
    new_pw=''

    while true ; do
        if command -v pwgen >/dev/null ; then
            new_pw=$(pwgen -s "${pw_length}" 1)
            break
        elif command -v openssl >/dev/null ; then
            # shellcheck disable=SC2086
            new_pw="${new_pw}$(openssl rand -base64 ${pw_length} | tr -dc '[:alnum:]')"
        else
            new_pw="${new_pw}$(head /dev/urandom | tr -dc '[:alnum:]')"
        fi
        # shellcheck disable=SC2000
        # shellcheck disable=SC2086
        [ "$(echo $new_pw | wc -c)" -ge "$pw_length" ] && break
    done

    echo "$new_pw" | cut -c1-"${pw_length}"
}

# arg: <user:locked:disabled:file:password:random:value...>
set_passwords()
{
    for _selector in ${1} ; do
        _username=$(echo "$_selector" | cut -d":" -f1)
        _locked=$(echo "$_selector" | cut -d":" -f2)
        _disabled=$(echo "$_selector" | cut -d":" -f3)
        _file=$(echo "$_selector" | cut -d":" -f4)
        _password=$(echo "$_selector" | cut -d":" -f5)
        _random=$(echo "$_selector" | cut -d":" -f6)
        _value=$(echo "$_selector" | cut -d":" -f7)

        # skip if user does not exist
        _uid=$(id -u "$_username" 2>/dev/null || true)
        if [ -z "$_uid" ] ; then
            msg "Password change: the user '${_username}' does not exist"
            continue
        fi

        case "$(uname | tr '[:upper:]' '[:lower:]')" in
            linux)
                if [ -n "$_disabled" ] ; then
                    msg "Password delete: ${_username}"
                    run_cmd passwd -d "$_username"
                elif [ -n "$_file" ] ; then
                    if [ -f "$_value" ] ; then
                        msg "Password change: ${_username}"
                        _newpasswd=$(sed -n 1p "$_value")
                        if command -v chpasswd > /dev/null ; then
                            # busybox
                            echo "${_username}:${_newpasswd}" | run_cmd chpasswd
                        else
                            echo "${_newpasswd}" | run_cmd passwd --stdin "$_username"
                        fi
                    else
                        err "Password file '${_value}' does not exist - ABORT"
                        exit 1
                    fi
                elif [ -n "$_password" ] ; then
                    msg "Password change: ${_username}"
                    if command -v chpasswd > /dev/null ; then
                        # busybox
                        echo "${_username}:${_value}" | run_cmd chpasswd
                    else
                        echo "${_value}" | run_cmd passwd --stdin "$_username"
                    fi
                elif [ -n "$_random" ] ; then
                    msg "Password change: ${_username}"
                    _newpasswd=$(gen_password)
                    if command -v chpasswd > /dev/null ; then
                        # busybox
                        echo "${_username}:${_newpasswd}" | run_cmd chpasswd
                    else
                        echo "${_newpasswd}" | run_cmd passwd --stdin "$_username"
                    fi
                fi

                if [ -n "$_locked" ] ; then
                    msg "Password lock: ${_username}"
                    run_cmd passwd -l "$_username"
                fi
                ;;
            freebsd)
                if [ -n "$_disabled" ] ; then
                    msg "Password delete: ${_username}"
                    run_cmd pw usermod -n "$_username" -h -
                elif [ -n "$_file" ] ; then
                    if [ -f "$_value" ] ; then
                        msg "Password change: ${_username}"
                        sed -n 1p "$_value" | \
                            run_cmd pw usermod -n "$_username" -h 0
                    else
                        err "Password file '${_value}' does not exist - ABORT"
                        exit 1
                    fi
                elif [ -n "$_password" ] ; then
                    msg "Password change: ${_username}"
                    echo "$_value" | \
                        run_cmd pw usermod -n "$_username" -h 0
                elif [ -n "$_random" ] ; then
                    msg "Password change: ${_username}"
                    _newpasswd=$(gen_password)
                    echo "$_newpasswd" | \
                        run_cmd pw usermod -n "$_username" -h 0
                fi

                if [ -n "$_locked" ] ; then
                    msg "Password lock: ${_username}"
                    IGNORE_ERROR=yes run_cmd pw lock "$_username"
                    IGNORE_ERROR=
                fi
                ;;
            *)
                warn "Password change: this system ('$(uname)') is not supported"
                ;;
        esac
    done
}

run_prep()
{
    # run compatibiltity/support check and abort here if 'strict' is used
    syscheck

    # --update ?
    if is_one_option 'update' ; then
        msg "Update the system..."

        if command -v apk >/dev/null ; then
            run_cmd apk --update-cache upgrade
        elif command -v apt-get >/dev/null ; then
            DEBIAN_FRONTEND=noninteractive
            export DEBIAN_FRONTEND

            run_cmd apt-get -qy update

            run_cmd apt-get -qy \
                -o Dpkg::Options::="--force-confdef" \
                -o DPkg::Options::="--force-confold" \
                upgrade

            run_cmd apt-get -qy autoremove
        elif command -v dnf >/dev/null ; then
            run_cmd dnf -y --best upgrade
        elif command -v yum >/dev/null ; then
            run_cmd yum -y --obsoletes update
        elif command -v zypper >/dev/null ; then
            run_cmd zypper --non-interactive update --auto-agree-with-licenses
        elif command -v pkg >/dev/null ; then
            # TODO: FreeBSD returns error when no update is available...
            run_cmd freebsd-update --not-running-from-cron fetch || true
            run_cmd freebsd-update --not-running-from-cron install || true
        else
            warn "No package manager found - SKIP"
        fi
    fi

    # --remove-user-accounts ?
    if [ -n "$ARG_USERS_REMOVE" ] ; then
        remove_arbitrary_users "$ARG_USERS_REMOVE" "$ARG_USERS_KEEP"
    fi

    # --root-password|--password ?
    if [ -n "$ARG_PASSWORD" ] ; then
        set_passwords "$ARG_PASSWORD"
    fi

    # TODO: this requires too much fiddling for just saving a user one command
    # if the single runlevel is requested then try to switch to it...or maybe
    # we already did that and we just want to continue...
    #if is_one_option 'single' ; then
    #    enter_single_mode
    #fi
}

run_last()
{
    # --selinux-relabel ?
    if is_one_option 'selinux-relabel' ; then
        msg "Try to relabel SELinux context..."

        # is this SELinux system?
        if [ "$(uname | tr '[:upper:]' '[:lower:]')" = 'linux' ] && \
            command -v restorecon >/dev/null ;
        then
            if command -v fixfiles >/dev/null ; then
                run_cmd fixfiles -f relabel
            else
                run_cmd touch /.autorelabel
            fi
        else
            warn "This not a SELinux enabled system - SKIP"
        fi
    fi

    # this does not hurt - sync to disk
    if command -v sync >/dev/null ; then
        msg "Running 'sync'..."
        sync
    fi

    # --poweroff?
    if is_one_option 'poweroff' ; then
        # firstly ask if not 'yes'
        ask_to_poweroff

        msg "We will try to turn off the machine..."
        case "$_ONE_OS_ID" in
            freebsd)
                run_cmd poweroff
                ;;
            *)
                if command -v poweroff >/dev/null ; then
                    run_cmd poweroff
                elif command -v shutdown >/dev/null ; then
                    run_cmd shutdown -p now
                elif command -v halt >/dev/null ; then
                    run_cmd halt -p
                else
                    err "Poweroff requested but no relevant command was found!"
                fi
                ;;
        esac
    fi
}

# arg: <list-of-operations>
run_ops()
{
    _all_ops=$(echo "$1" | tr ',' ' ')

    _whitelist=
    _blacklist=
    for _op in ${_all_ops} ; do
        # is it blacklisted operation?
        _black_op=$(echo "$_op" | sed -n 's/^-\(.*\)/\1/p')
        if [ -n "$_black_op" ] ; then
            # add excluded operation(s) to the blacklist
            _ops=$(get_operations "$_black_op")

            if [ -n "$_ops" ] ; then
                _blacklist="${_blacklist} ${_ops}"
            else
                err "Unsupported operation expr.: ${_op}"
                exit 1
            fi
        else
            # extend the whitelist for requested operation(s)
            _ops=$(get_operations "$_op")
            if [ -n "$_ops" ] ; then
                _whitelist="${_whitelist} ${_ops}"
            else
                err "Unsupported operation expr.: ${_op}"
                exit 1
            fi
        fi
    done

    # deduplicate and sort the both lists
    _whitelist=$(echo "$_whitelist" | tr ' ' '\n' | sort -u)
    _blacklist=$(echo "$_blacklist" | tr ' ' '\n' | sort -u)

    # filter out blacklisted operations and execute only the whitelisted ones
    _ops=
    _max_oplength=0
    # loop over the ALL available operations to honor the original order
    for _op in $(get_operations all) ; do
        # NOTE: on FreeBSD newlines are not preserved in the variable which is
        # strange because POSIX says that only trailing newlines are removed...

        # if the operation is NOT whitelisted/requested then skip it
        if ! echo "$_whitelist" | tr ' ' '\n' | grep -q "^${_op}\$" ; then
            continue
        fi

        # if the operation is NOT blacklisted then add it to the final list
        if ! echo "$_blacklist" | tr ' ' '\n' | grep -q "^${_op}\$" ; then
            # find the longest operation name
            if [ "${#_op}" -gt "$_max_oplength" ] ; then
                _max_oplength="${#_op}"
            fi
            _ops="${_ops} ${_op}"
        fi
    done

    # export arguments into operation subshells
    export ARG_ONE_OPTION_LIST
    export ARG_OPS_LIST
    export ARG_PASSWORD
    export ARG_USERS_KEEP
    export ARG_USERS_REMOVE

    # execute requested operations one by one
    for _op in ${_ops} ; do
        _op_func=$(echo "$_op" | tr '-' '_')
        if is_one_option 'verbose' ; then
            # run verbosely
            printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}:" \
                "Run operation"
            printf " ${SETCOLOR_ON_COMMAND}%s${SETCOLOR_OFF}\n" \
                "${_op}"

            # if-wrap
            if eval "op_${_op_func}" ; then
                _op_func_return_code=$?
            else
                _op_func_return_code=$?
            fi

            case "${_op_func_return_code}" in
                0)
                    printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}" \
                        "Result"
                    printf " ... ${SETCOLOR_ON_OK}%s${SETCOLOR_OFF}\n\n" \
                        "OK"
                    ;;
                1)
                    printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}" \
                        "Result"
                    printf " ... ${SETCOLOR_ON_FAILURE}%s${SETCOLOR_OFF}\n\n" \
                        "FAILED"
                    ;;
                2)
                    printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}" \
                        "Result"
                    printf " ... ${SETCOLOR_ON_FAILURE}%s${SETCOLOR_OFF}\n\n" \
                        "SKIPPED"
                    ;;
            esac
        else
            # run non-verbosely
            printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}:" \
                "Run operation"
            printf " ${SETCOLOR_ON_COMMAND}%-*s${SETCOLOR_OFF} ... " \
                "$_max_oplength" \
                "${_op}"

            # if-wrap
            if _op_func_output=$(eval "op_${_op_func}" 2>&1) ; then
                _op_func_return_code=$?
            else
                _op_func_return_code=$?
            fi

            case "${_op_func_return_code}" in
                0)
                    # shellcheck disable=SC2059
                    printf "${SETCOLOR_ON_OK}OK${SETCOLOR_OFF}\n"
                    ;;
                1)
                    # shellcheck disable=SC2059
                    printf "${SETCOLOR_ON_FAILURE}FAILED${SETCOLOR_OFF}\n"
                    echo "$_op_func_output"
                    ;;
                2)
                    # shellcheck disable=SC2059
                    printf "${SETCOLOR_ON_FAILURE}SKIPPED${SETCOLOR_OFF}\n"
                    echo "$_op_func_output"
                    ;;
            esac
        fi
    done
}

# arg: <option-list>
# this function initialize a global env. variable _ONE_OPTIONS based on the
# recognized options and force an abort when an unknown option is found
#
# NOTE: this is left as it is for when the need for '--one-options' rise again
# - that is why the options are replicated here...
#
# _ONE_OPTIONS is then used inside the is_one_option function for convenience
parse_one_options()
{
    # comb the list
    _all_options=$(echo "$1" | \
        sed -e 's/,/ /g' -e 's/[[:space:]]\+/\n/g' -e '/^$/d' | \
        sort -u)

    # leave empty
    _ONE_OPTIONS=
    _abort=
    for _option in ${_all_options} ; do
        case "$_option" in
            yes|strict|verbose|update|poweroff|selinux-relabel)
                _ONE_OPTIONS="${_ONE_OPTIONS} ${_option}"
                ;;
            *)
                err "Unknown option: ${_option}"
                _abort=yes
                ;;
        esac
    done

    # there were found unknown options - abort
    if [ -n "$_abort" ] ; then
        print_help
        exit 1
    fi
}

# arg: <option>
# it will simply check if option from argument was set (inside _ONE_OPTIONS)
is_one_option()
(
    for _option in ${_ONE_OPTIONS} ; do
        if [ "$1" = "$_option" ] ; then
            return 0
        fi
    done

    return 1
)

# inspired here:
# https://unix.stackexchange.com/questions/485070/all-round-100-portable-terminal-color-support-check
# and modified for alpine's busybox (which supports colors)
is_terminal_colorful()
{
    {
        command -v tput && { tput setaf || tput AF ; }
    } >/dev/null 2>&1

    # shellcheck disable=SC2181
    if [ $? -eq 0 ] ; then
        return 0
    fi

    # naive workaround for systems without ncurses
    test "$TERM" = 'xterm-256color'

    return $?
}

# args: <remove-user-list> <keep-user-list>
# if a user is in both lists then preserving will take precedence
# TODO: ERRROR FreeBSD does not save newlines in vars - echo | grep does not work!!!!!!!!!ยง
remove_arbitrary_users()
{
    # comb the lists
    _remove_list=$(echo "$1" | \
        sed -e 's/,/ /g' -e 's/[[:space:]]\+/\n/g' -e '/^$/d' | \
        sort -u)
    _keep_list=$(echo "$2" | \
        sed -e 's/,/ /g' -e 's/[[:space:]]\+/\n/g' -e '/^$/d' | \
        sort -u)

    # finalize the remove list by removing the preserved users
    _users=
    for _user in ${_remove_list} ; do
        # NOTE: on FreeBSD newlines are not preserved in the variable which is
        # strange because POSIX says that only trailing newlines are removed...
        if ! echo "$_keep_list" | tr ' '  '\n' | grep -q "^${_user}\$" ; then
            # user is not preserved so we can delete it if exists
            _uid=$(id -u "$_user" 2>/dev/null || true)
            if [ -n "$_uid" ] ; then
                _users="${_users} ${_user}:${_uid}"
            else
                msg "Removing user '${_user}': not present on the system"
            fi
        fi
    done

    case "$(uname | tr '[:upper:]' '[:lower:]')" in
        linux)
            for _user in ${_users} ; do
                _username=$(echo "$_user" | cut -d":" -f1)
                _uid=$(echo "$_user" | cut -d":" -f2)

                msg "Removing user '${_username}'..."

                # keep deluser first: alpine has both commands but does not
                # have the --selinux-user option for userdel...
                if command -v deluser >/dev/null ; then
                    run_cmd deluser --remove-home "$_username"
                elif command -v userdel >/dev/null ; then
                    run_cmd userdel --remove --selinux-user "$_username"
                fi

                # wipe-out all deleted user's files on the system
                IGNORE_ERROR=yes run_cmd find / -depth -user "$_uid" -delete
                IGNORE_ERROR=
            done
            ;;
        freebsd)
            for _user in ${_users} ; do
                _username=$(echo "$_user" | cut -d":" -f1)
                _uid=$(echo "$_user" | cut -d":" -f2)

                msg "Removing user '${_username}'..."

                run_cmd pw userdel "$_username" -r

                # wipe-out all deleted user's files on the system
                IGNORE_ERROR=yes run_cmd find / -depth -user "$_uid" -delete
                IGNORE_ERROR=
            done
            ;;
        *)
            warn "Removing users: this system ('$(uname)') is not supported"
            ;;
    esac
}

################################################################################
# sysprep operations
#

# reimplemented operations

op_abrt_data()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /var/spool/abrt/*
)

op_backup_files()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op find / \( -name '*.bak' -o -name '*~' \) -delete || true
)

op_bash_history()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /home/*/.bash_history /root/.bash_history
)

op_blkid_tab()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /var/run/blkid.tab \
        /var/run/blkid.tab.old \
        /etc/blkid/blkid.tab \
        /etc/blkid/blkid.tab.old \
        /etc/blkid.tab \
        /etc/blkid.tab.old \
        /dev/.blkid.tab \
        /dev/.blkid.tab.old \
        ;
)

op_ca_certificates()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for f in \
        /etc/pki/CA/certs/*.crt \
        /etc/pki/CA/crl/*.crt \
        /etc/pki/CA/newcerts/*.crt \
        /etc/pki/CA/private/*.key \
        /etc/pki/tls/private/*.key \
        /etc/pki/tls/certs/*.crt \
    ; do
        case "$f" in
            /etc/pki/tls/certs/ca-bundle.crt)
                # skip
                :;;
            /etc/pki/tls/certs/ca-bundle.trust.crt)
                # skip
                :;;
            *)
                # all else delete
                run_op rm -vf "$f"
                ;;
        esac
    done
)

op_crash_data()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /var/crash/* /var/log/dump/*
)

op_cron_spool()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for d in \
        /var/spool/cron/ \
        /var/spool/cron/atjobs/ \
        /var/spool/atjobs/ \
        /var/spool/atspool/ \
        /var/spool/at/ \
    ; do
        if [ -d "$d" ] ; then
            run_op find "$d" -type f -not -name .SEQ -delete
            find "$d" -type f -name .SEQ -print | while read -r _seq_file ; do
                echo "+ truncate ${_seq_file}"
                cat /dev/null > "$_seq_file"
            done
        fi
    done
)

# TODO: should we support customize?
op_customize()
(
    echo NOOP
)

op_dhcp_client_state()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /var/lib/dhclient/* /var/lib/dhcp/*
)

op_dhcp_server_state()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /var/lib/dhcpd/*
)

op_dovecot_data()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /var/lib/dovecot/*
)

op_firewall_rules()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /etc/sysconfig/iptables \
        /etc/firewalld/services/* \
        /etc/firewalld/zones/* \
        ;
)

op_flag_reconfiguration()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op touch /.unconfigured
)

# TODO: is this possible on mounted fs?
op_fs_uuids()
(
    echo NOOP
)

op_kerberos_data()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for f in /var/kerberos/krb5kdc/* ; do
        case "$f" in
            /var/kerberos/krb5kdc/kadm5.acl)
                # skip
                :;;
            /var/kerberos/krb5kdc/kdc.conf)
                # skip
                :;;
            *)
                # all else delete
                run_op rm -vf "$f"
                ;;
        esac
    done
)

# TODO: can this be improved? Stopping all services, syslog etc.?
# TODO: can the list be extended? This is copy-pasted from the latest libguestfs
op_logfiles()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf \
        /var/log/*.log* \
        /var/log/audit/* \
        /var/log/btmp* \
        /var/log/cron* \
        /var/log/dmesg* \
        /var/log/lastlog* \
        /var/log/maillog* \
        /var/log/mail/* \
        /var/log/messages* \
        /var/log/secure* \
        /var/log/spooler* \
        /var/log/tallylog* \
        /var/log/wtmp* \
        /var/log/apache2/*_log \
        /var/log/apache2/*_log-* \
        /var/log/ntp \
        /var/log/tuned/tuned.log \
        /var/log/debug* \
        /var/log/syslog* \
        /var/log/faillog* \
        /var/log/firewalld* \
        /var/log/grubby* \
        /var/log/xferlog* \
        /var/log/BackupPC/LOG \
        /var/log/ceph/*.log \
        /var/log/chrony/*.log \
        /var/log/cups/*_log* \
        /var/log/glusterfs/*glusterd.vol.log \
        /var/log/glusterfs/glusterfs.log \
        /var/log/httpd/*log \
        /var/log/jetty/jetty-console.log \
        /var/log/libvirt/libxl/*.log \
        /var/log/libvirt/libvirtd.log \
        /var/log/libvirt/lxc/*.log \
        /var/log/libvirt/qemu/*.log \
        /var/log/libvirt/uml/*.log \
        /var/named/data/named.run \
        /var/log/ppp/connect-errors \
        /var/log/setroubleshoot/*.log \
        /var/log/squid/*.log \
        /var/lib/logrotate.status \
        /root/install.log \
        /root/install.log.syslog \
        /root/anaconda-ks.cfg \
        /root/anaconda-post.log \
        /root/initial-setup-ks.cfg \
        /root/original-ks.cfg \
        /var/log/anaconda.syslog \
        /var/log/anaconda/* \
        /var/log/installer/* \
        /var/cache/gdm/* \
        /var/lib/AccountService/users/* \
        /var/lib/fprint/* \
        /var/cache/fontconfig/* \
        /var/cache/man/* \
        /var/log/sa/* \
        /var/log/gdm/* \
        /var/log/lightdm/* \
        /var/log/ntpstats/* \
        /etc/Pegasus/*.cnf \
        /etc/Pegasus/*.crt \
        /etc/Pegasus/*.csr \
        /etc/Pegasus/*.pem \
        /etc/Pegasus/*.srl \
        /var/log/rhsm/* \
        /var/log/journal/* \
        /var/log/aptitude* \
        /var/log/apt/* \
        /var/log/exim4/* \
        /var/log/ConsoleKit/* \
        /var/log/landscape/* \
        /var/log/unattended-upgrades/* \
        ;
)

# TODO: can ths be implemented?
op_lvm_uuids()
(
    echo NOOP
)

op_machine_id()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for f in /etc/machine-id /var/lib/dbus/machine-id ; do
        if [ -f "$f" ] ; then
            echo "+ truncate ${f}"
            cat /dev/null > "$f"
        fi
    done
)

op_mail_spool()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /var/spool/mail/* /var/mail/*
)

# this is redhat-centric but fixed in one-network and one-hostname
op_net_hostname()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for f in /etc/sysconfig/network-scripts/ifcfg-* ; do
        if [ -f "$f" ] ; then
            run_op sed -i -e '/^HOSTNAME=/d' -e '/^DHCP_HOSTNAME=/d' "$f"
        fi
    done
)

# this is redhat-centric but fixed in one-network
op_net_hwaddr()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for f in /etc/sysconfig/network-scripts/ifcfg-* ; do
        if [ -f "$f" ] ; then
            run_op sed -i -e '/^HWADDR=/d' "$f"
        fi
    done
)

op_pacct_log()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    if [ -f /var/account/pacct ] ; then
        run_op rm -vf /var/account/pacct*
        run_op touch /var/account/pacct
    fi

    if [ -f /var/log/account/pacct ] ; then
        run_op rm -vf /var/log/account/pacct*
        run_op touch /var/log/account/pacct
    fi
)

# this one is extended from libguestfs sysprep version
op_package_manager_cache()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    if command -v apk >/dev/null ; then
        run_op apk cache clean
    elif command -v apt-get >/dev/null ; then
        run_op apt-get clean
    elif command -v apt >/dev/null ; then
        run_op apt clean
    elif command -v dnf >/dev/null ; then
        run_op dnf clean all --enablerepo=\*
    elif command -v yum >/dev/null ; then
        run_op yum clean all --enablerepo=\*
    elif command -v zypper >/dev/null ; then
        run_op zypper clean --all
    elif command -v pkg >/dev/null ; then
        run_op pkg clean --all --yes
    fi


    for d in \
        /var/cache/apt/archives/ \
        /var/cache/dnf/ \
        /var/cache/yum/ \
        /var/cache/zypp* \
        /var/cache/apk/* \
    ; do
        if [ -d "$d" ] ; then
            run_op find "$d" -type f -delete
        fi
    done
)

op_pam_data()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /var/run/console/* \
        /var/run/faillock/* \
        /var/run/sepermit/* \
        ;
)

op_passwd_backups()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /etc/group- \
        /etc/gshadow- \
        /etc/passwd- \
        /etc/shadow- \
        /etc/subuid- \
        /etc/subgid- \
        ;
)

op_puppet_data_log()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /var/log/puppet/* \
        /var/lib/puppet/*/* \
        /var/lib/puppet/*/*/* \
        ;
)

op_rhn_systemid()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /etc/sysconfig/rhn/systemid \
        /etc/sysconfig/rhn/osad-auth.conf \
        ;
)

op_rh_subscription_manager()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /etc/pki/consumer/* /etc/pki/entitlement/*
)

op_rpm_db()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /var/lib/rpm/__db.*
)

op_samba_db_log()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /var/log/samba/old/* \
        /var/log/samba/* \
        /var/lib/samba/*/* \
        /var/lib/samba/* \
        ;
)

op_script()
(
    echo NOOP
)

op_smolt_uuid()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /etc/sysconfig/hw-uuid \
        /etc/smolt/uuid \
        /etc/smolt/hw-uuid \
        ;
)

op_ssh_hostkeys()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /etc/ssh/*_host_*
)

op_ssh_userdir()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -rvf /home/*/.ssh /root/.ssh
)

op_sssd_db_log()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /var/log/sssd/* \
        /var/lib/sss/db/* \
        ;
)

op_tmp_files()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    for d in /tmp/ /var/tmp/ ; do
        if [ -d "$d" ] ; then
            run_op find "$d" -maxdepth 1 -mindepth 1 -exec rm -rvf '{}' \;
        fi
    done
)

op_udev_persistent_net()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /etc/udev/rules.d/70-persistent-net.rules
)

# it honors the --keep-user-accounts list
op_user_account()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    if [ "${_ONE_VALID_AWK}" = 'NO' ] ; then
        printf "[!] This test is skipped due to the incompatible awk implementation!\n"
        return 2
    fi

    # comb the list
    _keep_list=$(echo "$ARG_USERS_KEEP" | \
        sed -e 's/,/ /g' -e 's/[[:space:]]\+/\n/g' -e '/^$/d' | \
        sort -u)

    case "$(uname | tr '[:upper:]' '[:lower:]')" in
        linux)
            if [ -f /etc/login.defs ] ; then
                _uid_min=$(</etc/login.defs \
                    awk '/^[[:space:]]*UID_MIN[[:space:]]/ {print $2}')
                _uid_max=$(</etc/login.defs \
                    awk '/^[[:space:]]*UID_MAX[[:space:]]/ {print $2}')

                # setup the default values according to manual pages for login.defs
                if [ -z "$_uid_min" ] ; then
                    _uid_min=1000
                fi
                if [ -z "$_uid_max" ] ; then
                    _uid_max=60000
                fi

                _users=$(getent passwd | \
                    awk -v uid_min="$_uid_min" -v uid_max="$_uid_max" \
                        '
                        BEGIN {
                            FS = ":";
                        }
                        {
                            if (($3 >= uid_min) && ($3 <= uid_max))
                                printf("%s:%s\n", $1, $3);
                        }' | sort -u)

                for _user in ${_users} ; do
                    _username=$(echo "$_user" | cut -d":" -f1)
                    _uid=$(echo "$_user" | cut -d":" -f2)

                    # is this user preserved?
                    # NOTE: on FreeBSD newlines are not preserved in the variable
                    # which is strange because POSIX says that only trailing
                    # newlines are removed...just to be on the safe side I will
                    # reintroduce them on the linux too...
                    if echo "$_keep_list" | tr ' ' '\n' | grep -q "^${_username}\$" ; then
                        continue
                    fi

                    # keep deluser first: alpine has both commands but does not
                    # have the --selinux-user option for userdel...
                    if command -v deluser >/dev/null ; then
                        run_op deluser --remove-home "$_username"
                    elif command -v userdel >/dev/null ; then
                        run_op userdel --remove --selinux-user "$_username"
                    fi

                    # wipe-out all deleted user's files on the system
                    run_op find / -depth -user "$_uid" -delete || true
                done
            else
                err "No /etc/login.defs - we cannot guess the MIN-MAX of UIDs/GIDs"
                return 1
            fi
            ;;
        freebsd)
            if [ -f /etc/pw.conf ] ; then
                _uid_min=$(</etc/pw.conf \
                    awk '/^[[:space:]]*minuid[[:space:]]/ {print $2}')
                _uid_max=$(</etc/pw.conf \
                    awk '/^[[:space:]]*maxuid[[:space:]]/ {print $2}')
            fi

            # setup the default values according to manual pages for pw.conf
            if [ -z "$_uid_min" ] ; then
                _uid_min=1000
            fi
            if [ -z "$_uid_max" ] ; then
                _uid_max=32000
            fi

            _users=$(getent passwd | \
                awk -v uid_min="$_uid_min" -v uid_max="$_uid_max" \
                    '
                    BEGIN {
                        FS = ":";
                    }
                    {
                        if (($3 >= uid_min) && ($3 <= uid_max))
                            printf("%s:%s\n", $1, $3);
                    }' | sort -u)

            for _user in ${_users} ; do
                _username=$(echo "$_user" | cut -d":" -f1)
                _uid=$(echo "$_user" | cut -d":" -f2)

                # is this user preserved?
                # NOTE: on FreeBSD newlines are not preserved in the variable
                # which is strange because POSIX says that only trailing
                # newlines are removed...
                if echo "$_keep_list" | tr ' ' '\n' | grep -q "^${_username}\$" ; then
                    continue
                fi

                run_op pw userdel "$_username" -r

                # wipe-out all deleted user's files on the system
                run_op find / -depth -user "$_uid" -delete || true
            done
            ;;
        *)
            warn "Removing users: this system ('$(uname)') is not supported"
            ;;
    esac
)

op_utmp()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /var/run/utmp
)

op_yum_uuid()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf /var/lib/yum/uuid
)

# extra one operations

op_one_cleanup()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vrf \
        /var/lib/one-context/ \
        ;
)

op_one_shell_history()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    run_op rm -vf \
        /home/*/.history \
        /root/.history \
        /home/*/.*_history \
        /root/.*_history \
        ;
)

# TODO: this is disabled for now because it depends on user-account and due to
# the alphabetical order this is executed sooner which then causes problem...
op_one_group_account()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    if [ "${_ONE_VALID_AWK}" = 'NO' ] ; then
        printf "[!] This test is skipped due to the incompatible awk implementation!\n"
        return 2
    fi

    case "$(uname | tr '[:upper:]' '[:lower:]')" in
        linux)
            if [ -f /etc/login.defs ] ; then
                _gid_min=$(</etc/login.defs \
                    awk '/^[[:space:]]*GID_MIN[[:space:]]/ {print $2}')
                _gid_max=$(</etc/login.defs \
                    awk '/^[[:space:]]*GID_MAX[[:space:]]/ {print $2}')

                # setup the default values according to manual pages for login.defs
                if [ -z "$_gid_min" ] ; then
                    _gid_min=1000
                fi
                if [ -z "$_gid_max" ] ; then
                    _gid_max=60000
                fi

                _groups=$(getent group | \
                    awk -v gid_min="$_gid_min" -v gid_max="$_gid_max" \
                        '
                        BEGIN {
                            FS = ":";
                        }
                        {
                            if (($3 >= gid_min) && ($3 <= gid_max))
                                printf("%s:%s\n", $1, $3);
                        }' | sort -u)

                for _group in ${_groups} ; do
                    _groupname=$(echo "$_group" | cut -d":" -f1)
                    _gid=$(echo "$_group" | cut -d":" -f2)

                    if command -v groupdel >/dev/null ; then
                        run_op groupdel "$_groupname"
                    elif command -v delgroup >/dev/null ; then
                        run_op delgroup "$_groupname"
                    fi

                    # wipe-out all deleted group's files on the system
                    run_op find / -depth -group "$_gid" -delete || true
                done
            else
                err "No /etc/login.defs - we cannot guess the MIN-MAX of UIDs/GIDs"
                return 1
            fi
            ;;
        freebsd)
            if [ -f /etc/pw.conf ] ; then
                _gid_min=$(</etc/pw.conf \
                    awk '/^[[:space:]]*mingid[[:space:]]/ {print $2}')
                _gid_max=$(</etc/pw.conf \
                    awk '/^[[:space:]]*maxgid[[:space:]]/ {print $2}')
            fi

            # setup the default values according to manual pages for pw.conf
            if [ -z "$_gid_min" ] ; then
                _gid_min=1000
            fi
            if [ -z "$_gid_max" ] ; then
                _gid_max=32000
            fi

            _groups=$(getent group | \
                awk -v gid_min="$_gid_min" -v gid_max="$_gid_max" \
                    '
                    BEGIN {
                        FS = ":";
                    }
                    {
                        if (($3 >= gid_min) && ($3 <= gid_max))
                            printf("%s:%s\n", $1, $3);
                    }' | sort -u)

            for _group in ${_groups} ; do
                _groupname=$(echo "$_group" | cut -d":" -f1)
                _gid=$(echo "$_group" | cut -d":" -f2)

                run_op pw groupdel "$_groupname"

                # wipe-out all deleted group's files on the system
                run_op find / -depth -group "$_gid" -delete || true
            done
            ;;
        *)
            warn "Removing groups: this system ('$(uname)') is not supported"
            ;;
    esac
)

op_one_hostname()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    # wipe-out hostname
    if [ -f /etc/hostname ] ; then
        echo "+ echo localhost"
        echo localhost > /etc/hostname
    fi

    # wipe-out hosts
    cat > /etc/hosts <<EOF
# The following lines are desirable for IPv4 capable hosts
127.0.0.1       localhost

# The following lines are desirable for IPv6 capable hosts
::1             localhost ip6-localhost ip6-loopback
fe00::0         ip6-localnet
ff00::0         ip6-mcastprefix
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
ff02::3         ip6-allhosts
EOF
)

op_one_resolvconf()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    # wipe-out resolv.conf
    echo "+ nameserver 127.0.0.1"
    echo nameserver 127.0.0.1 > /etc/resolv.conf
)

op_one_network()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    case "$(uname | tr '[:upper:]' '[:lower:]')" in
        linux)
            if [ -f /etc/network/interfaces ] ; then
                for f in /etc/network/interfaces* ; do
                    if [ -f "$f" ] ; then
                        run_op rm -vf /etc/network/interfaces*
                    fi
                done
                echo "+ reset /etc/network/interfaces"
                cat > /etc/network/interfaces <<EOF
auto lo
iface lo inet loopback
EOF
            fi

            if [ -f /etc/networks ] ; then
                echo "+ reset /etc/networks"
                cat > /etc/networks <<EOF
default		0.0.0.0
loopback	127.0.0.0
link-local	169.254.0.0
EOF
            fi

            # systemd
            run_op rm -rvf /etc/systemd/network/*

            # redhat/suse scripts
            if [ -d /etc/sysconfig/network-scripts ] || \
               [ -d /etc/sysconfig/network/ ] ;
            then
                run_op find /etc/sysconfig/network*/ \
                    -maxdepth 1 \
                    -type f \
                    \( -name ifcfg-\* -a -not -name ifcfg-lo \) \
                    -delete
                run_op find /etc/sysconfig/network*/ \
                    -maxdepth 1 \
                    -type f \
                    \( -name route-\* -o -name ifroute-\* \) \
                    -delete
            fi
            ;;
        freebsd)
            run_op rm -vf /etc/hostid
            run_op rm -vf /etc/rc.conf.d/network
            ;;
        *)
            warn "Cleaning network settings: this system ('$(uname)') is not supported"
            ;;
    esac

    # wipe-out hosts.allow|deny|etc files
    if [ -f /etc/hosts.allow ] ; then
        echo "+ clear /etc/hosts.allow"
        cat /dev/null > /etc/hosts.allow
    fi
    if [ -f /etc/hosts.deny ] ; then
        echo "+ clear /etc/hosts.deny"
        cat /dev/null > /etc/hosts.deny
    fi
    if [ -f /etc/hosts.equiv ] ; then
        echo "+ clear /etc/hosts.equiv"
        cat /dev/null > /etc/hosts.equiv
    fi
    if [ -f /etc/hosts.lpd ] ; then
        echo "+ clear /etc/hosts.lpd"
        cat /dev/null > /etc/hosts.lpd
    fi
)

# fill up the free space with zeroes to claim the whole free/unused space for
# sparsify or trim (below) - to fully utilize this - one should create virtual
# disk similar to this (on QEMU/KVM):
#   <disk type='file' device='disk'>
#       <driver name='qemu' type='raw' discard='unmap'/>
#       <source file='/some.img'/>
#       <target dev='sda' bus='scsi'/>
#       <address type='drive' controller='0' bus='0' target='0' unit='0'/>
#   </disk>
op_one_zerofill()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    msg "Fill the free space with zeroes and delete it"

    # 1MiB - busybox workaround
    _bytes=$(( 1024 * 1024 ))
    run_op dd if=/dev/zero of=/.zero bs=${_bytes} || true
    run_op sync
    run_op rm -f /.zero
)

# trim/discard the unused space
# TODO: FreeBSD version will break if the device path has some whitespace which
# is very unlikely...
op_one_trim()
(
    run_op() { echo "+ ${*}" ; "$@" ; }

    case "$(uname | tr '[:upper:]' '[:lower:]')" in
        linux)
            msg "Trim/discard the unused space"

            if command -v fstrim > /dev/null ; then
                run_op fstrim -a -v || run_op fstrim -v /
            else
                warn "Missing fstrim tool - SKIP"
            fi
            ;;
        freebsd)
            # TODO: I don't know what device to use here - let that be an
            # improvement for the future...
            #msg "Trim/discard the unused space"

            # TODO: check awk sanity if used...

            #if command -v trim > /dev/null ; then
            #    _devs=$(< /etc/fstab awk '
            #        {
            #            if ($0 !~ /^[[:space:]]*#/)
            #                print $1;
            #        }')

            #    for _dev in ${_devs} ; do
            #        run_op trim -f "$_dev"
            #    done
            #else
            #    warn "Missing fstrim tool - SKIP"
            #fi
            warn "Trim not supported on the FreeBSD - SKIP"
            ;;
    esac
)

################################################################################
# main
#

# are we colorful?
if is_terminal_colorful ; then
    SETCOLOR_ON_OK='\033[0;32;1m' # green
    SETCOLOR_ON_FAILURE='\033[0;31;1m' # red
    SETCOLOR_ON_HIGHLIGHT='\033[0;32;1m' # green
    SETCOLOR_ON_HIGHLIGHT2='\033[0;36;1m' # cyan
    SETCOLOR_ON_COMMAND='\033[0;33;1m' # yellow
    SETCOLOR_OFF='\033[0m'
else
    SETCOLOR_ON_OK=
    SETCOLOR_ON_FAILURE=
    SETCOLOR_ON_HIGHLIGHT=
    SETCOLOR_ON_HIGHLIGHT2=
    SETCOLOR_ON_COMMAND=
    SETCOLOR_OFF=
fi

# print banner
cat <<EOF
   ___   _ __    ___
  / _ \ | '_ \  / _ \   OpenNebula System Preparation Tool
 | (_) || | | ||  __/   (c) 2002-2021, OpenNebula Project, OpenNebula Systems
  \___/ |_| |_| \___|   Version 6.4.0

EOF

# leave this empty
IGNORE_ERROR=
ARG_ONE_OPTION_LIST=
ARG_OPS_LIST=
ARG_PASSWORD=
ARG_USERS_KEEP=
ARG_USERS_REMOVE=

# parse arguments
state=nil
action=
abort=
while [ $# -gt 0 ] ; do
    case "$state" in
        nil)
            case "$1" in
                --help)
                    state=help
                    case "$action" in
                        '')
                            action=help
                            ;;
                        help)
                            err "Redundant argument: ${1}"
                            ;;
                        *)
                            err "Arguments mismatch"
                            abort=yes
                            ;;
                    esac
                    ;;
                --list-operations)
                    state=list
                    case "$action" in
                        '')
                            action=list-operations
                            ;;
                        list-operations)
                            err "Redundant argument: ${1}"
                            ;;
                        *)
                            err "Arguments mismatch"
                            abort=yes
                            ;;
                    esac
                    ;;
                --operations)
                    state=ops
                    case "$action" in
                        ''|operations)
                            action=operations
                            ;;
                        *)
                            err "Arguments mismatch"
                            abort=yes
                            ;;
                    esac
                    ;;
                # DISABLED:
                #--one-options)
                #    state=options
                #    ;;
                --yes|--strict|--update|--verbose|--poweroff|--selinux-relabel)
                    ARG_ONE_OPTION_LIST="${ARG_ONE_OPTION_LIST},${1##--}"
                    ;;
                --root-password)
                    state=root-password
                    ;;
                --password)
                    state=password
                    ;;
                --remove-user-accounts)
                    state=remove-users
                    ;;
                --keep-user-accounts)
                    state=keep-users
                    ;;
                *)
                    err "Unknown argument: ${1}"
                    abort=yes
                    ;;
            esac
            ;;
        help)
            err "Extra argument: ${1}"
            abort=yes
            ;;
        list)
            err "Extra argument: ${1}"
            abort=yes
            ;;
        ops)
            ARG_OPS_LIST="${ARG_OPS_LIST},${1}"
            state=nil
            ;;
        # DISABLED:
        #options)
        #    ARG_ONE_OPTION_LIST="${ARG_ONE_OPTION_LIST},${1}"
        #    state=nil
        #    ;;
        root-password)
            ARG_PASSWORD="${ARG_PASSWORD},root:${1}"
            state=nil
            ;;
        password)
            ARG_PASSWORD="${ARG_PASSWORD},${1}"
            state=nil
            ;;
        remove-users)
            ARG_USERS_REMOVE="${ARG_USERS_REMOVE},${1}"
            state=nil
            ;;
        keep-users)
            ARG_USERS_KEEP="${ARG_USERS_KEEP},${1}"
            state=nil
            ;;
    esac

    # there were usage errors - abort
    if [ -n "$abort" ] ; then
        print_help
        exit 1
    fi

    shift
done

# verify that arguments are not short of values...
case "$state" in
    ops|options|root-password|password|remove-users|keep-users)
        err "Missing argument(s)"
        exit 1
        ;;
esac

# validate one options
parse_one_options "$ARG_ONE_OPTION_LIST"

# validate password arguments and save them again in parsed format
ARG_PASSWORD=$(parse_password_arguments "$ARG_PASSWORD")

# execute the requested action
case "$action" in
    '')
        sanity_check

        # ask before running
        ask_to_run_sysprep

        # run default operations
        run_prep
        run_ops default
        ;;
    help)
        usage
        exit 0
        ;;
    list-operations)
        list_operations
        exit 0
        ;;
    operations)
        sanity_check

        # ask before running
        ask_to_run_sysprep

        run_prep
        run_ops "$ARG_OPS_LIST"
        ;;
esac

# optional last sysprep steps (like poweroff)
run_last

msg "DONE"

exit 0

security is just an illusion