# ------------------------------------------------------------------------------ # # Functions for OpenSLX-NG # # ------------------------------------------------------------------------------ # # check if we have our environment variables to check # if we actually got sourced by the main script if [ -z "${SELF_PID}" -o -z "${ROOT_DIR}" ]; then # not using perror, since we probably don't have it. echo "Neither SELF_PID nor ROOT_DIR is set. Was this included by OpenSLX-NG?" exit 1 fi # ------------------------------------------------------------------------------ # # General helper functions # # ------------------------------------------------------------------------------ banner () { echo -e "\033[38;5;196m\t"' .__ ' echo -e "\033[38;5;196m\t"' ____ ______ ____ ____ _____| | ___ ___ ' echo -e "\033[38;5;202m\t"' / _ \\\\____ \\_/ __ \\ / \\ / ___/ | \\ \\/ / ' echo -e "\033[38;5;208m\t"'( <_> ) |_> > ___/| | \\\\___ \\| |__> < ' echo -e "\033[38;5;214m\t"' \\____/| __/ \\___ >___| /____ >____/__/\\_ \\ ' echo -e "\033[38;5;220m\t"' |__| \\/ \\/ \\/ \\/ ' echo -e "\033[38;5;220m\t" echo -e "\033[38;5;226m\t ** OpenSLX Project // 2015 **" echo -e "\033[38;5;232m\t http://lab.openslx.org/" echo -e "\033[00m" } pinfo() { echo -e "\033[38;5;10m[info]\033[0m $@" } pwarning() { echo -e "\033[38;5;11m[warning]\033[0m $@" } perror() { echo -e "\033[38;5;9m[error]\033[0m $@" kill "$SELF_PID" exit 1 } # accepts one argument to be printed as a warning before dumping the help print_usage() { [ -n "$1" ] && pwarning "$1" pinfo "USAGE:" pinfo "$ARG0 " pinfo "\t\tActions: '--clone', '--package', '--export', 'update'" pinfo "" pinfo "CLONING: rsync remote host to local directory" pinfo "$ARG0 --clone --host [--syncdir ]" pinfo "\t\tIf not specified, --syncdir = './clones//stage4'" pinfo "" pinfo "PACKAGING: pack local rsync directory as qcow2-container" pinfo "$ARG0 --package --host [--container ] [--syncdir ]" pinfo "\t\tIf is not specified, --container = './clones//stage4.qcow2'" pinfo "\t\tIf is not specified, --syncdir = './clones//stage4'" pinfo "" pinfo "EXPORTING: rsync remote host to a new qcow2-container" pinfo "$ARG0 --export --host [--container ] [--syncdir ]" pinfo "\t\tIf is not specified, --container = './clones//stage4.qcow2'" pinfo "" pinfo "UPDATING: rsync remote host to an existing qcow2-container" pinfo "$ARG0 --export --host [--container ]" pinfo "\t\tIf is not specified, --container = './clones//stage4.qcow2'" kill "$SELF_PID" exit 1 } # helper to parse the command line arguments and fill the environment # with the parameters given. Since the fallbacks for those variables # are only dependent on the selected action, we will also post-process # them to make sure they are set when leaving this function! read_params() { # initialize global variables declare -g FORCE=0 unset ACTION REMOTE_HOST CONTAINER_PATH RSYNC_TARGET # handle rest of arguments while [ "$#" -gt "0" ]; do local PARAM="$1" shift # options to current target if [[ "$PARAM" == --* ]]; then case "$PARAM" in --clone) declare -rg ACTION="CLONE" ;; --package) declare -rg ACTION="PACKAGE" ;; --export) declare -rg ACTION="EXPORT" ;; --update) declare -rg ACTION="UPDATE" ;; --host) if [ -z "$1" ]; then print_usage "'--host' requires a host as parameter." else if [[ "$1" == --* ]]; then local _msg="A host should not start with '--'." _msg="$_msg Parameter for '--host' missing?" perror $_msg fi declare -rg REMOTE_HOST="$1" shift fi continue ;; --container) if [ -z "$1" ]; then print_usage "'--container' requires a path as parameter." else if [[ "$1" == --* ]]; then local _msg="A path should not start with '--'." _msg="$_msg Parameter for '--container' missing?" perror $_msg fi declare -rg CONTAINER_PATH="$1" shift fi continue ;; --syncdir) if [ -z "$1" ]; then print_usage "'--syncdir' requires a path as parameter." else if [[ "$1" == --* ]]; then local _msg="A path should not start with '--'." _msg="$_msg Parameter for '--syncdir' missing?" perror $_msg fi declare -rg RSYNC_TARGET="$1" shift fi continue ;; --force) declare -rg FORCE=1 ;; *) print_usage "Unknown flag: $PARAM" ;; esac continue fi done # done parsing the arguments, exit if no action given [ -z "$ACTION" ] && \ print_usage "No action given" # now check for existance of variables # and use fallbacks when possible if they were not specified # REMOTE_HOST valid? always needed! [ -z "$REMOTE_HOST" ] && \ perror "REMOTE_HOST not set. Use '--host'." check_host "$REMOTE_HOST" || \ perror "'$REMOTE_HOST' is neither an IP nor a known hostname." # REMOTE_HOST valid - set build directory for the rest of the operations declare -rg BUILD_DIR="${ROOT_DIR}/clones/${REMOTE_HOST}" mkdir -p "${BUILD_DIR}" || perror "Could not create '${BUILD_DIR}'." # RSYNC_TARGET needs special care if [ -z "$RSYNC_TARGET" ]; then # none given - use fallbacks if [ "x$ACTION" == "xCLONE" ]; then # use default path when cloning, no need for CONTAINER_MNT pwarning "RSYNC_TARGET not set, using: '${BUILD_DIR}/stage4/'." declare -rg RSYNC_TARGET="${BUILD_DIR}/stage4" else # we always want CONTAINER_MNT here declare -rg CONTAINER_MNT="$(mktemp -d)" [ -z "${CONTAINER_MNT}" ] && \ perror "Could not create temporary directory for mounting the container." # RSYNC_TARGET depends on the action at this point if [ "x$ACTION" == "xPACKAGE" ]; then # use default path when packaging declare -rg RSYNC_TARGET="${BUILD_DIR}/stage4" elif [ "x$ACTION" == "xUPDATE" -o "x$ACTION" == "xEXPORT" ]; then # for action update/export, we want to sync to the mounted container # so create a temporary directory for the mount point that we'll use later declare -rg RSYNC_TARGET="${CONTAINER_MNT}" fi fi fi # CONTAINER_PATH valid? if [ -z "$CONTAINER_PATH" ]; then # use default path: ${BUILD_DIR}/stage4.qcow2 pwarning "CONTAINER_PATH not set. Using '${BUILD_DIR}/stage4.qcow2'." declare -rg CONTAINER_PATH="${BUILD_DIR}/stage4.qcow2" fi # so from now on REMOTE_HOST, RSYNC_TARGET, CONTAINER_PATH are set (and read-only). } process_action() { if [ "x$ACTION" == "xCLONE" ]; then clone_host || perror "Cloning stage4 failed with: $?" elif [ "x$ACTION" == "xPACKAGE" ]; then pack_clone || perror "Packing as QCoW2 failed with: $?" elif [ "x$ACTION" == "xEXPORT" ]; then export_host || perror "Exporting failed with: $?" elif [ "x$ACTION" == "xUPDATE" ]; then update_container || perror "Updating failed with: $?" else print_usage "No action given." fi return 0 } # ------------------------------------------------------------------------------ # # Stage4 related functions # # ------------------------------------------------------------------------------ # # Helper to generate a stage4 export for a remote machine per rsync. # Usage: # clone_host # # Note: this functions requires REMOTE_HOST and RSYNC_TARGET to be set. clone_host() { # check if RSYNC_TARGET is valid if [ -d "${RSYNC_TARGET}" ]; then # does it have the '.stage4' flag? [ ! -e "${RSYNC_TARGET}/.stage4" ] && \ perror "'${RSYNC_TARGET}' exists, but no '.stage4' flag found." \ "Refusing to rsync there." else # not a directory, create it and set the .stage4 flag mkdir -p "${RSYNC_TARGET}" || perror "Could not create '${RSYNC_TARGET}'." fi local EXCLUDE="$BUILD_DIR/exclude-stage4" local INCLUDE="$BUILD_DIR/include-stage4" pinfo "Building rsync include/exclude files for building stage4...." echo "## Exclude file for stage4 of $REMOTE_HOST" > "$EXCLUDE" echo "## Include file for stage4 of $REMOTE_HOST" > "$INCLUDE" for FILE in $(find "${ROOT_DIR}"/blacklists/*/ -type f); do echo "## From $FILE" >> "$EXCLUDE" echo "## From $FILE" >> "$INCLUDE" grep '^-' "$FILE" >> "$EXCLUDE" grep '^+' "$FILE" >> "$INCLUDE" done # prepare rsync's options if [ -z "$DEFAULT_RSYNC_OPTS" ]; then local RSYNC_OPTS="-e ssh -c arcfour -oStrictHostKeyChecking=no" else local RSYNC_OPTS="$DEFAULT_RSYNC_OPTS" fi local RSYNC_SOURCE="root@$REMOTE_HOST:/" _STATE='SYNCING' # run rsync with the exclude/include lists created earlier cat "$INCLUDE" "$EXCLUDE" | \ rsync --verbose \ --acls \ --hard-links \ --xattrs \ --archive \ --delete \ --delete-excluded \ --numeric-ids \ --exclude-from=- \ "${RSYNC_OPTS}" \ "${RSYNC_SOURCE}" \ "${RSYNC_TARGET}" \ || perror "rsync from '${RSYNC_SOURCE}' to '${RSYNC_TARGET}' failed." ## TODO real exit code handling pinfo "Cloning '${REMOTE_HOST}' to '${RSYNC_TARGET}' succeeded." touch "${RSYNC_TARGET}/.stage4" _STATE='SYNCED' return 0 } # Helper to create the empty container at CONTAINER_PATH create_container() { # CONTAINER_PATH valid? [ -d "$CONTAINER_PATH" ] && perror "Path to container can not be a directory!" if [ -f "$CONTAINER_PATH" ]; then if [ $FORCE -eq 0 ]; then perror "Container file already exists. Use '--force' to overwrite it." else # force removal rm -f "$CONTAINER_PATH" || perror "Could not remove '$CONTAINER_PATH'" pinfo "Removed old '$CONTAINER_PATH'." fi fi # which size for the qcow2 container? if [ -z "$DEFAULT_QCOW_SIZE" ]; then local QCOW_SIZE="25G" else local QCOW_SIZE="$DEFAULT_QCOW_SIZE" fi # so far so good pinfo "Creating qcow2-container '${CONTAINER_PATH}'" qemu-img create -f qcow2 "${CONTAINER_PATH}" "${QCOW_SIZE}" || \ perror "qemu-img create failed with: $?" # now expose it as a loop device expose_container _STATE='QCOW_EXPOSED' # filesystem for the qcow2 container? if [ -z "$DEFAULT_QCOW_FS" ]; then local QCOW_FS="ext4" else # check if we have mkfs helper which "mkfs.$DEFAULT_QCOW_FS" &>/dev/null || \ perror "Could not find 'mkfs.$DEFAULT_QCOW_FS'. Install it and retry." local QCOW_FS="$DEFAULT_QCOW_FS" fi pinfo "Creating '${QCOW_FS}' filesystem on '${CONTAINER_PATH}'..." mkfs."${QCOW_FS}" "${LOOPED_NBD_DEV}" || perror "mkfs failed with: $?" } # Helper exposing the container as a loop device expose_container() { [ -z "${CONTAINER_PATH}" ] && \ perror "Internal error - CONTAINER_PATH not set but should be! Check read_params()" # find usable nbd device declare -rg NBD_DEV="$(find_free_nbd)" [ -n "${NBD_DEV}" ] || perror "Could not find usable NBD device." [ -b "${NBD_DEV}" ] || perror "'${NBD_DEV}' is not a block device!" pinfo "Connecting '${CONTAINER_PATH}' to '${NBD_DEV}'" qemu-nbd -c "${NBD_DEV}" "${CONTAINER_PATH}" || \ perror "qemu-nbd failed with: $?" _STATE='QCOW_EXPOSED_NBD' # expose as a loop device declare -rg LOOPED_NBD_DEV="$(losetup --find)" losetup "${LOOPED_NBD_DEV}" "${NBD_DEV}" || \ perror "Loop device setup for '${NBD_DEV}' failed with: $?" } # Helper to mount CONTAINER_PATH to CONTAINER_MNT through expose_container mount_container() { [ -z "${CONTAINER_MNT}" ] && \ perror "Internal error - CONTAINER_MNT not set but should be! Check read_params()" # connect container to a loop device first, if it wasnt already done [ -z "${LOOPED_NBD_DEV}" ] && expose_container # lets be safe... [ -z "${LOOPED_NBD_DEV}" ] && \ perror "Internal error - LOOPED_NBD_DEV not set but should be! Check expose_container()" # now we got everything, mount it pinfo "Mounting '${LOOPED_NBD_DEV}' to '${CONTAINER_MNT}'..." mount "${LOOPED_NBD_DEV}" "${CONTAINER_MNT}" || perror "Mount failed with: $?" _STATE='QCOW_MOUNTED' } # helper to copy the content of RSYNC_TARGET to CONTAINER_MNT copy_to_container() { [ -z "${RSYNC_TARGET}" ] && \ perror "Internal error - RSYNC_TARGET not set but should be!" [ -z "${CONTAINER_MNT}" ] && \ perror "Internal error - CONTAINER_MNT not set but should be!" # sanity checks is_dir_empty "$RSYNC_TARGET" && \ perror "'$RSYNC_TARGET' appears to be empty. Did you clone?" # check for '.stage4' flag in the directory, indicating we cloned there if [ ! -e "${RSYNC_TARGET}/.stage4" ]; then perror "No '.stage4' flag found in '${RSYNC_TARGET}'." \ "Was this cloned properly?" fi # copy files from the stage4 directory to the mounted qcow2-container pinfo "Copying '${RSYNC_TARGET}' to '${CONTAINER_MNT}'..." rsync -avAHX "${RSYNC_TARGET}"/ "${CONTAINER_MNT}"/ || perror "Rsync failed with: $?" } # wrapper to package a cloned stage4 as a qcow2 container # - creates empty container at CONTAINER_PATH # - mounts it to RSYNC_TARGET # - copy RSYNC_TARGET pack_clone() { create_container mount_container copy_to_container umount_container } # wrapper to update an existing container # - mounts it to RSYNC_TARGET # - clone host there update_container() { mount_container clone_host umount_container } # wrapper to export a host directly to a container # - create en empty qcow2 container at CONTAINER_PATH # - mount it to RSYNC_TARGET # - clone host there export_host() { create_container mount_container clone_host umount_container } # helper function trapped on SIGTERM/SIGINT # Usage: do not use as is cleanexit() { trap '' SIGINT SIGTERM # from now on, ignore INT and TERM pwarning "SIGINT/SIGTERM triggered - cleaning up ..." case "${_STATE}" in SYNCING) # error during rsync, create the .stage4 file again [ -z "${RSYNC_TARGET}" ] && \ perror "RSYNC_TARGET not set, this should not happen." if [ ! -e "${RSYNC_TARGET}/.stage4" ]; then pwarning "'.stage4' flag was lost during rsync, restoring it." touch "${RSYNC_TARGET}/.stage4" fi break; ;; QCOW_MOUNTED) # container mounted [ -z "${CONTAINER_PATH}" ] && \ perror "CONTAINER_PATH not set - should not be!" [ -z "${CONTAINER_MNT}" ] && \ perror "CONTAINER_MNT not set - should not be!" umount_container || \ perror "Could not umount '${CONTAINER_MNT}'." ;; QCOW_MOUNTED|QCOW_EXPOSED) disconnect_container break ;; # internal expose_container state QCOW_EXPOSED_NBD) disconnect_nbd break ;; *) [ -n "${_STATE}" ] && pwarning "Unknown state: ${_STATE}" ;; esac # still here? then we ran into some error exit 1 } # Helper to umount + disconnect the container from all the devices umount_container() { [ -z "${CONTAINER_MNT}" ] && \ perror "CONTAINER_MNT not set - is it really mounted?" umount "${CONTAINER_MNT}" || \ perror "Failed to umount '${CONTAINER_MNT}'." rmdir "${CONTAINER_MNT}" || \ pwarning "Could not remove '${CONTAINER_MNT}'." } # Wrapper to disconnect the container from all the devices disconnect_container() { disconnect_loop disconnect_nbd } # Helper to disconnect from loop device disconnect_loop() { [ -z "${LOOPED_NBD_DEV}" ] && \ perror "Container not connected to a loop device?" losetup -d "${LOOPED_NBD_DEV}" ||\ perror "Could not disconnect loop device '${LOOPED_NBD_DEV}'." } # Helper to disconnect from nbd device disconnect_nbd() { [ -z "${NBD_DEV}" ] && \ perror "Container does not seem to be connected to a NBD device?" qemu-nbd -d "${NBD_DEV}" || \ perror "Could not disconnect '${CONTAINER_PATH}' from '${NBD_DEV}'." } # helper to find an unused nbd device # Usage: # find_free_nbd # Echoes the name of the free device to stdout, empty string otherwise. find_free_nbd() { local nbd_size=0 for nbd_id in {0..15}; do [ -b "/dev/nbd${nbd_id}" ] || continue [ -r "/sys/block/nbd${nbd_id}/size" ] || continue nbd_size=$(cat /sys/block/nbd${nbd_id}/size) [ $nbd_size -eq 0 ] && echo "/dev/nbd${nbd_id}" && break done echo "" } # helper to validate an ip # Usage: # valid_ip # Returns 0 if valid, 1 otherwise. valid_ip() { local ip=$1 local stat=1 if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then OIFS=$IFS IFS='.' ip=($ip) IFS=$OIFS [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] stat=$? fi return $stat } # helper to check whether a given host is valid # Usage: # check_host # Returns 0 if valid, 1 otherwise check_host() { local HOST="$1" [ -z "$HOST" ] && return 1 # check if its a valid IP or a valid hostname valid_ip "$HOST" && return 0 host -W 2 "$HOST" && return 0 # still here? then fail return 1 } # helper to check if a dir is empty or not # Usage: # dir_empty # Returns 0 if empty, 1 otherwise is_dir_empty() { [ $# -ne 1 ] && perror "$0 requires directory as paramter, none given." local _dir="$1" [ -d "$_dir" ] || return 1 [ -n "$(ls -A $_dir)" ] && return 1 || return 0 } # helper to ask user for confirmation # Usage: # user_confirm # Return 0 if confirmed, 1 otherwise user_confirm() { [ $# -ne 1 ] && perror "$0 requires the question as first argument." pinfo "$1 [Y/n]" local _input read _input [ "x${_input}" == "x" -o "x${_input}" == "xy" ] && return 0 || return 1 }