diff --git a/src/usr/sbin/onesysprep b/src/usr/sbin/onesysprep new file mode 100755 index 0000000..e73c9ab --- /dev/null +++ b/src/usr/sbin/onesysprep @@ -0,0 +1,2227 @@ +#!/bin/sh + +# ---------------------------------------------------------------------------- # +# Copyright 2020, 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} ${*}" +VERSION='5.13.80' + +# here are declared all sysprep operations - keep the following format: +# :: +# for each operation there should be corresponding function: +# op_ +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-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 <]... [--yes] [--strict] [--update] + [--selinux-relabel] [--remove-user-accounts ] + [--keep-user-accounts ] [--root-password ] + [--password ] [--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): ... +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 +} + +# arg: [normal|summary] +# if used with argument 'summary' then it will print out summary of supported +# operations - otherwise it will output parseable: : +parse_sysprep_operations() +( + echo "$ALL_SYSPREP_OPERATIONS" | awk -v output="${1:-normal}" ' + BEGIN { + FS = ":"; + count=0; + } + { + if ($0 == "") + next; + + op = $1; + sub(/^[[:space:]]*/, "", op); + sub(/[[:space:]]*$/, "", op); + + default_op = $2; + sub(/^[[:space:]]*/, "", default_op); + sub(/[[:space:]]*$/, "", default_op); + + comment = $3; + sub(/^[[:space:]]*/, "", comment); + sub(/[[:space:]]*$/, "", 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| +# 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 +) + +# 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:]') + 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) + return 0 + ;; + opensuse*) + 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|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|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: +# return: user:locked:disabled:file:password:random:value... +parse_password_arguments() +{ + _raw_password_list=$(echo "$1" | tr ',' ' ') + _parsed_password_list= + + # selector should have format: + # :[locked:]disabled|file:|password:|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: +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 + 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 + [ "$(echo $new_pw | wc -c)" -ge "$pw_length" ] && break + done + + echo "$new_pw" | cut -c1-${pw_length} +} + +# arg: +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: +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 eval "op_${_op_func}" ; then + printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}" \ + "Result" + printf " ... ${SETCOLOR_ON_OK}%s${SETCOLOR_OFF}\n\n" \ + "OK" + else + printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}" \ + "Result" + printf " ... ${SETCOLOR_ON_FAILURE}%s${SETCOLOR_OFF}\n\n" \ + "FAILED" + fi + else + # run non-verbosely + printf "${SETCOLOR_ON_HIGHLIGHT}%s${SETCOLOR_OFF}:" \ + "Run operation" + printf " ${SETCOLOR_ON_COMMAND}%-*s${SETCOLOR_OFF} ... " \ + "$_max_oplength" \ + "${_op}" + if _op_func_output=$(eval "op_${_op_func}" 2>&1) ; then + # shellcheck disable=SC2059 + printf "${SETCOLOR_ON_OK}OK${SETCOLOR_OFF}\n" + else + # shellcheck disable=SC2059 + printf "${SETCOLOR_ON_FAILURE}FAILED${SETCOLOR_OFF}\n" + echo "$_op_func_output" + fi + fi + done +} + +# arg: +# 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: